445 lines
23 KiB
TypeScript
445 lines
23 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
||
import { MatrixViz } from "../components/MatrixViz";
|
||
import { useLanguage } from "../App";
|
||
import {
|
||
Play,
|
||
Pause,
|
||
FileText,
|
||
Headphones,
|
||
Zap,
|
||
Volume2,
|
||
X,
|
||
} from "lucide-react";
|
||
|
||
export const BlogPost: React.FC = () => {
|
||
const { language } = useLanguage();
|
||
const [activeSection, setActiveSection] = useState("intro");
|
||
const [activeTab, setActiveTab] = useState<"read" | "listen">("read");
|
||
const [isPlaying, setIsPlaying] = useState(false);
|
||
const [showMiniPlayer, setShowMiniPlayer] = useState(false);
|
||
|
||
// Simple scroll spy logic
|
||
useEffect(() => {
|
||
const handleScroll = () => {
|
||
const sections = ["intro", "methods", "results", "discussion"];
|
||
for (const section of sections) {
|
||
const element = document.getElementById(section);
|
||
if (element) {
|
||
const rect = element.getBoundingClientRect();
|
||
if (rect.top >= 0 && rect.top <= 300) {
|
||
setActiveSection(section);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
window.addEventListener("scroll", handleScroll);
|
||
return () => window.removeEventListener("scroll", handleScroll);
|
||
}, []);
|
||
|
||
const handleMainPlayPause = () => {
|
||
const newState = !isPlaying;
|
||
setIsPlaying(newState);
|
||
if (newState) {
|
||
setShowMiniPlayer(true);
|
||
}
|
||
};
|
||
|
||
const content = {
|
||
en: {
|
||
title: "LoRA Without Regret",
|
||
subtitle: "John Schulman in collaboration with Pradit",
|
||
date: "Sep 29, 2025",
|
||
tldr_title: "Executive Summary (TL;DR)",
|
||
tldr_text:
|
||
"Low-Rank Adaptation (LoRA) is a technique to fine-tune large models efficiently. Our research confirms that while LoRA saves 90% of memory, it can match full fine-tuning performance only if the rank (r) is sufficiently high and alpha scaling is tuned correctly. Ideal for post-training but not pre-training.",
|
||
tabs: { read: "Read Article", listen: "Listen (TTS)" },
|
||
intro_1:
|
||
"Today’s leading language models contain upwards of a trillion parameters, pretrained on tens of trillions of tokens. Base model performance keeps improving with scale, as these trillions are necessary for learning and representing all the patterns in written-down human knowledge.",
|
||
intro_2:
|
||
"In contrast, post-training involves smaller datasets and generally focuses on narrower domains of knowledge and ranges of behavior. It seems wasteful to use a terabit of weights to represent updates from a gigabit or megabit of training data. This intuition has motivated parameter efficient fine-tuning (PEFT), which adjusts a large network by updating a much smaller set of parameters.",
|
||
method_title: "The Method",
|
||
method_1:
|
||
"The leading PEFT method is low-rank adaptation, or LoRA. LoRA replaces each weight matrix W from the original model with a modified version W' = W + γBA, where B and A are matrices that together have far fewer parameters than W, and γ is a constant scaling factor.",
|
||
method_2:
|
||
"In effect, LoRA creates a low-dimensional representation of the updates imparted by fine-tuning. LoRA may offer advantages in the cost and speed of post-training, and there are also a few operational reasons to prefer it to full fine-tuning.",
|
||
results_title: "Operational Advantages",
|
||
results_list: [
|
||
{
|
||
title: "Multi-tenant serving",
|
||
text: "Since LoRA trains an adapter (i.e., the A and B matrices) while keeping the original weights unchanged, a single inference server can keep many adapters in memory.",
|
||
},
|
||
{
|
||
title: "Layout size for training",
|
||
text: "FullFT usually requires an order of magnitude more accelerators than sampling from the same model does.",
|
||
},
|
||
{
|
||
title: "Ease of loading",
|
||
text: "With fewer weights to store, LoRA adapters are fast and easy to set up or transfer between machines.",
|
||
},
|
||
],
|
||
conclusion_title: "Conclusion",
|
||
conclusion_text:
|
||
"The question remains: can LoRA match the performance of full fine-tuning? Our internal benchmarks suggest yes, provided rank scaling is carefully managed.",
|
||
nav: {
|
||
intro: "Introduction",
|
||
methods: "Methods",
|
||
results: "Results",
|
||
discussion: "Conclusion",
|
||
},
|
||
now_playing: "Now Playing",
|
||
paused: "Paused",
|
||
transcript_title: "Transcript",
|
||
},
|
||
th: {
|
||
title: "LoRA โดยไม่เสียใจ",
|
||
subtitle: "John Schulman ร่วมกับ Pradit (ประดิษฐ์)",
|
||
date: "29 กันยายน 2025",
|
||
tldr_title: "บทสรุปผู้บริหาร (TL;DR)",
|
||
tldr_text:
|
||
"Low-Rank Adaptation (LoRA) เป็นเทคนิคการปรับแต่งโมเดลขนาดใหญ่ที่มีประสิทธิภาพ งานวิจัยของเรายืนยันว่าแม้ LoRA จะประหยัดหน่วยความจำได้ถึง 90% แต่จะให้ผลลัพธ์เทียบเท่าการ Fine-tune แบบเต็มรูปแบบได้ก็ต่อเมื่อค่า Rank (r) สูงเพียงพอและมีการปรับค่า Alpha อย่างถูกต้อง เหมาะสำหรับขั้นตอน Post-training แต่ไม่เหมาะกับ Pre-training",
|
||
tabs: { read: "อ่านบทความ", listen: "ฟังเสียงบรรยาย" },
|
||
intro_1:
|
||
"โมเดลภาษาชั้นนำในปัจจุบันมีพารามิเตอร์มากกว่าล้านล้านตัว ผ่านการฝึกฝนบนข้อมูลมหาศาล ประสิทธิภาพของโมเดลพื้นฐานดีขึ้นเรื่อยๆ ตามขนาด ซึ่งจำเป็นสำหรับการเรียนรู้รูปแบบความรู้ทั้งหมดของมนุษย์",
|
||
intro_2:
|
||
"ในทางตรงกันข้าม การฝึกฝนหลังเสร็จสิ้น (Post-training) มักใช้ชุดข้อมูลขนาดเล็กและเน้นขอบเขตความรู้ที่แคบกว่า ดูเหมือนจะสิ้นเปลืองที่จะใช้น้ำหนักระดับเทราบิตเพื่อแทนการอัปเดตจากข้อมูลเพียงกิกะบิต แนวคิดนี้จึงนำไปสู่การปรับแต่งแบบประหยัดพารามิเตอร์ (PEFT)",
|
||
method_title: "วิธีการ",
|
||
method_1:
|
||
"วิธี PEFT ชั้นนำคือ Low-rank adaptation หรือ LoRA ซึ่งแทนที่เมทริกซ์น้ำหนัก W แต่ละตัวด้วยเวอร์ชันแก้ไข W' = W + γBA โดยที่ B และ A เป็นเมทริกซ์ที่มีพารามิเตอร์น้อยกว่า W มาก",
|
||
method_2:
|
||
"โดยสรุป LoRA สร้างตัวแทนมิติที่ต่ำกว่าของการอัปเดตที่เกิดจากการ Fine-tuning LoRA อาจมีข้อได้เปรียบในด้านต้นทุนและความเร็ว และยังมีเหตุผลด้านการปฏิบัติงานที่ทำให้เป็นที่นิยมมากกว่า Full Fine-tuning",
|
||
results_title: "ข้อได้เปรียบด้านการปฏิบัติงาน",
|
||
results_list: [
|
||
{
|
||
title: "การให้บริการแบบ Multi-tenant",
|
||
text: "เนื่องจาก LoRA ฝึกฝน Adapter (เมทริกซ์ A และ B) โดยไม่เปลี่ยนน้ำหนักเดิม เซิร์ฟเวอร์เดียวจึงสามารถเก็บ Adapter หลายตัวในหน่วยความจำได้",
|
||
},
|
||
{
|
||
title: "ขนาด Layout สำหรับการฝึก",
|
||
text: "FullFT มักต้องการตัวเร่งความเร็ว (GPU/TPU) มากกว่าการสุ่มตัวอย่างจากโมเดลเดียวกันถึงหนึ่งระดับ (Order of magnitude)",
|
||
},
|
||
{
|
||
title: "ความง่ายในการโหลด",
|
||
text: "ด้วยน้ำหนักที่ต้องเก็บน้อยกว่า LoRA adapters จึงตั้งค่าและโอนย้ายระหว่างเครื่องได้รวดเร็ว",
|
||
},
|
||
],
|
||
conclusion_title: "บทสรุป",
|
||
conclusion_text:
|
||
"คำถามยังคงอยู่: LoRA สามารถทำงานได้เทียบเท่ากับ Full Fine-tuning หรือไม่? การทดสอบภายในของเราชี้ว่า 'ได้' หากมีการจัดการ Rank scaling อย่างระมัดระวัง",
|
||
nav: {
|
||
intro: "บทนำ",
|
||
methods: "วิธีการ",
|
||
results: "ผลลัพธ์",
|
||
discussion: "บทสรุป",
|
||
},
|
||
now_playing: "กำลังเล่น",
|
||
paused: "หยุดชั่วคราว",
|
||
transcript_title: "คำบรรยาย",
|
||
},
|
||
};
|
||
|
||
const t = content[language];
|
||
|
||
return (
|
||
<div className="bg-paper min-h-screen pb-24 relative">
|
||
{/* Article Header */}
|
||
<header className="pt-24 pb-8 max-w-4xl mx-auto px-6 text-center">
|
||
<h1 className="font-serif text-4xl md:text-5xl text-ink leading-tight mb-6">
|
||
{t.title}
|
||
</h1>
|
||
<div className="font-sans text-sm text-subtle uppercase tracking-widest mb-2">
|
||
{t.subtitle}
|
||
</div>
|
||
<time className="font-serif italic text-subtle">{t.date}</time>
|
||
|
||
{/* View Tabs */}
|
||
<div className="flex justify-center mt-12 mb-8">
|
||
<div className="inline-flex bg-gray-100 p-1 rounded-lg border border-gray-200">
|
||
<button
|
||
onClick={() => setActiveTab("read")}
|
||
className={`flex items-center px-6 py-2 rounded-md text-sm font-bold transition-all ${activeTab === "read" ? "bg-white text-ink shadow-sm" : "text-subtle hover:text-ink"}`}
|
||
>
|
||
<FileText size={16} className="mr-2" />
|
||
{t.tabs.read}
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("listen")}
|
||
className={`flex items-center px-6 py-2 rounded-md text-sm font-bold transition-all ${activeTab === "listen" ? "bg-white text-ink shadow-sm" : "text-subtle hover:text-ink"}`}
|
||
>
|
||
<Headphones size={16} className="mr-2" />
|
||
{t.tabs.listen}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* TL;DR Section */}
|
||
<div className="max-w-4xl mx-auto px-6 mb-12">
|
||
<div className="bg-blue-50 border border-blue-100 p-6 md:p-8 rounded-sm relative overflow-hidden">
|
||
<div className="absolute top-0 left-0 w-1 h-full bg-accent"></div>
|
||
<div className="flex items-start gap-4">
|
||
<div className="bg-white p-2 rounded-full shadow-sm text-accent shrink-0 mt-1">
|
||
<Zap size={20} />
|
||
</div>
|
||
<div>
|
||
<h3 className="font-sans font-bold text-xs uppercase tracking-widest text-accent mb-2">
|
||
{t.tldr_title}
|
||
</h3>
|
||
<p className="font-serif text-ink leading-relaxed text-base md:text-lg">
|
||
{t.tldr_text}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{activeTab === "listen" ? (
|
||
/* Voice Interface */
|
||
<div className="max-w-5xl mx-auto px-4 sm:px-6 animate-in fade-in zoom-in duration-300">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||
{/* Left: Sticky Player */}
|
||
<div className="md:sticky md:top-32 h-fit">
|
||
<div className="bg-[#1e1e1e] rounded-xl p-8 text-center shadow-2xl border border-gray-800">
|
||
<div className="w-20 h-20 bg-gray-800 rounded-full mx-auto mb-6 flex items-center justify-center shadow-inner relative">
|
||
{isPlaying && (
|
||
<span className="absolute w-full h-full rounded-full border-2 border-green-500 animate-ping opacity-20"></span>
|
||
)}
|
||
<Headphones size={28} className="text-gray-400" />
|
||
</div>
|
||
|
||
<h3 className="text-white font-serif text-xl mb-2">
|
||
{t.title}
|
||
</h3>
|
||
<p className="text-gray-400 font-mono text-[10px] mb-8">
|
||
Voice: Gemini 2.5 Flash TTS
|
||
</p>
|
||
|
||
{/* Fake Waveform */}
|
||
<div className="h-12 flex items-center justify-center gap-1 mb-8 px-4 overflow-hidden">
|
||
{[...Array(30)].map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className={`w-1 bg-green-500 transition-all duration-150 rounded-full ${isPlaying ? "animate-pulse" : ""}`}
|
||
style={{
|
||
height: isPlaying ? `${Math.random() * 100}%` : "10%",
|
||
opacity: isPlaying ? 1 : 0.3,
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex items-center justify-center gap-6">
|
||
<button className="text-gray-400 hover:text-white transition-colors font-mono text-xs">
|
||
1.0x
|
||
</button>
|
||
<button
|
||
onClick={handleMainPlayPause}
|
||
className="w-14 h-14 bg-white rounded-full flex items-center justify-center hover:scale-105 transition-transform text-black shadow-lg"
|
||
>
|
||
{isPlaying ? (
|
||
<Pause fill="black" size={20} />
|
||
) : (
|
||
<Play fill="black" size={20} className="ml-1" />
|
||
)}
|
||
</button>
|
||
<button className="text-gray-400 hover:text-white transition-colors font-mono text-xs">
|
||
-15s
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Scrollable Transcript */}
|
||
<div className="flex flex-col h-[600px]">
|
||
<div className="font-sans text-xs font-bold uppercase tracking-widest text-subtle mb-4 flex items-center gap-2">
|
||
<FileText size={14} /> {t.transcript_title}
|
||
</div>
|
||
<div className="flex-grow overflow-y-auto pr-4 space-y-6 text-subtle font-serif leading-relaxed border-l-2 border-gray-100 pl-6 custom-scrollbar">
|
||
<p className="text-ink font-medium">{t.intro_1}</p>
|
||
<p>{t.intro_2}</p>
|
||
<div className="h-px bg-gray-100 w-full my-4"></div>
|
||
<p className="text-xs font-sans font-bold text-gray-400 uppercase">
|
||
{t.method_title}
|
||
</p>
|
||
<p>{t.method_1}</p>
|
||
<p>{t.method_2}</p>
|
||
<div className="h-px bg-gray-100 w-full my-4"></div>
|
||
<p className="text-xs font-sans font-bold text-gray-400 uppercase">
|
||
{t.results_title}
|
||
</p>
|
||
{t.results_list.map((item, idx) => (
|
||
<p key={idx}>
|
||
<strong className="text-gray-700">{item.title}:</strong>{" "}
|
||
{item.text}
|
||
</p>
|
||
))}
|
||
<div className="h-px bg-gray-100 w-full my-4"></div>
|
||
<p>{t.conclusion_text}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Read Interface */
|
||
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 lg:grid-cols-12 gap-12 pb-20">
|
||
{/* Left Sidebar - TOC */}
|
||
<aside className="hidden lg:block lg:col-span-3 relative">
|
||
<nav className="sticky top-32 space-y-4">
|
||
<ul className="font-serif text-sm text-subtle space-y-3 border-l border-gray-200 pl-4">
|
||
<li>
|
||
<a
|
||
href="#intro"
|
||
className={`block transition-colors ${activeSection === "intro" ? "text-ink font-bold" : "hover:text-ink"}`}
|
||
>
|
||
{t.nav.intro}
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a
|
||
href="#methods"
|
||
className={`block transition-colors ${activeSection === "methods" ? "text-ink font-bold" : "hover:text-ink"}`}
|
||
>
|
||
{t.nav.methods}
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a
|
||
href="#results"
|
||
className={`block transition-colors ${activeSection === "results" ? "text-ink font-bold" : "hover:text-ink"}`}
|
||
>
|
||
{t.nav.results}
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a
|
||
href="#discussion"
|
||
className={`block transition-colors ${activeSection === "discussion" ? "text-ink font-bold" : "hover:text-ink"}`}
|
||
>
|
||
{t.nav.discussion}
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</aside>
|
||
|
||
{/* Main Content */}
|
||
<article className="col-span-1 lg:col-span-6 font-serif text-lg leading-relaxed text-gray-800">
|
||
<MatrixViz />
|
||
|
||
<section id="intro" className="mb-12">
|
||
<p className="mb-6 first-letter:float-left first-letter:text-7xl first-letter:pr-4 first-letter:font-bold first-letter:text-ink">
|
||
{t.intro_1}
|
||
</p>
|
||
<p className="mb-6">{t.intro_2}</p>
|
||
</section>
|
||
|
||
<section id="methods" className="mb-12">
|
||
<h2 className="font-sans font-bold text-xl text-ink mt-12 mb-6">
|
||
{t.method_title}
|
||
</h2>
|
||
<p className="mb-6">{t.method_1}</p>
|
||
<p className="mb-6">{t.method_2}</p>
|
||
</section>
|
||
|
||
<section id="results" className="mb-12">
|
||
<h3 className="font-sans font-bold text-lg text-ink mt-8 mb-4">
|
||
{t.results_title}
|
||
</h3>
|
||
<ul className="list-disc list-outside ml-6 space-y-4 mb-6 text-base">
|
||
{t.results_list.map((item, idx) => (
|
||
<li key={idx}>
|
||
<strong className="text-ink">{item.title}.</strong>{" "}
|
||
{item.text}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
|
||
<section id="discussion" className="mb-12">
|
||
<h2 className="font-sans font-bold text-xl text-ink mt-12 mb-6">
|
||
{t.conclusion_title}
|
||
</h2>
|
||
<p className="mb-6">{t.conclusion_text}</p>
|
||
</section>
|
||
</article>
|
||
|
||
{/* Right Sidebar - Footnotes */}
|
||
<aside className="hidden lg:block lg:col-span-3 relative">
|
||
<div className="sticky top-32 space-y-12">
|
||
<div className="text-xs text-subtle font-serif leading-relaxed border-l-2 border-gray-100 pl-3">
|
||
<sup className="font-bold mr-1">1</sup>
|
||
Punica: Multi-Tenant LoRA Serving (Chen, Ye, et al, 2023)
|
||
</div>
|
||
<div className="text-xs text-subtle font-serif leading-relaxed border-l-2 border-gray-100 pl-3">
|
||
<sup className="font-bold mr-1">2</sup>
|
||
LoRA: Low-Rank Adaptation of Large Language Models (Hu et al,
|
||
2021)
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</main>
|
||
)}
|
||
|
||
{/* Mini Player Overlay - Persistent if showMiniPlayer is true */}
|
||
{showMiniPlayer && activeTab === "read" && (
|
||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-40 animate-in slide-in-from-bottom-4 fade-in">
|
||
<div className="bg-ink text-white pl-4 pr-4 py-3 rounded-full shadow-2xl flex items-center gap-4 border border-gray-700">
|
||
<div className="flex items-center gap-3 border-r border-gray-700 pr-4">
|
||
<div className="relative flex h-3 w-3">
|
||
{isPlaying && (
|
||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||
)}
|
||
<span
|
||
className={`relative inline-flex rounded-full h-3 w-3 ${isPlaying ? "bg-green-500" : "bg-yellow-500"}`}
|
||
></span>
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-[10px] font-sans font-bold uppercase tracking-wider text-gray-400">
|
||
{isPlaying ? t.now_playing : t.paused}
|
||
</span>
|
||
<span className="text-xs font-bold font-serif truncate max-w-[150px]">
|
||
{t.title}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setIsPlaying(!isPlaying)}
|
||
className="hover:text-green-400 transition-colors p-1"
|
||
title={isPlaying ? "Pause" : "Play"}
|
||
>
|
||
{isPlaying ? (
|
||
<Pause size={20} fill="currentColor" />
|
||
) : (
|
||
<Play size={20} fill="currentColor" />
|
||
)}
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("listen")}
|
||
className="text-xs font-mono underline hover:text-gray-300 px-2"
|
||
>
|
||
{language === "en" ? "Transcript" : "คำบรรยาย"}
|
||
</button>
|
||
|
||
<div className="w-px h-4 bg-gray-700 mx-1"></div>
|
||
|
||
<button
|
||
onClick={() => {
|
||
setIsPlaying(false);
|
||
setShowMiniPlayer(false);
|
||
}}
|
||
className="text-gray-400 hover:text-red-400 transition-colors p-1"
|
||
title="Close Player"
|
||
>
|
||
<X size={18} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|