[{"content":"Nối tiếp bài fine-tuning LLM ở lần trước, giả sử chúng ta có một model 70B đã fine-tune xong, benchmark đẹp, demo trên máy cá nhân mượt. Nhưng khi deploy lên production cho user thật sử dụng — giả sử 5000 người cùng gõ prompt và enter cùng lúc — liệu kết quả có còn đẹp như demo?\nKhông. Hệ thống sẽ nghẽn, GPU báo Out-of-memory. Vấn đề không nằm ở model, mà ở cách chúng ta serving nó.\nBài này đi từ những khái niệm nền tảng nhất — inference pipeline, prefill/decode, KV Cache, latency, throughput — rồi dần đi sâu vào các kỹ thuật đứng sau những serving engine hiện đại: PagedAttention, prefix caching, continuous batching, chunked prefill, và cuối cùng là cách chọn engine nào cho use case nào.\nServing LLM khác gì serving các model ML truyền thống? Trong machine learning truyền thống, model serving thường được hình dung khá đơn giản: nhận request, gom thành batch, chạy một forward pass, trả prediction.\nNhưng với LLM thì khác. Nó sinh văn bản theo cơ chế autoregressive: sau khi xử lý prompt, model phải liên tục sinh từng token, trong đó token tiếp theo phụ thuộc vào toàn bộ ngữ cảnh đã sinh trước đó — ví dụ với prompt \u0026ldquo;Once upon\u0026rdquo;, model sinh \u0026ldquo;a\u0026rdquo;, rồi ghép \u0026ldquo;Once upon a\u0026rdquo; làm input cho bước kế tiếp để sinh \u0026ldquo;time\u0026rdquo;, cứ thế tiếp tục.\nMột request vì thế không còn là một phép inference duy nhất, mà trở thành một chuỗi các bước inference có trạng thái và thời gian sống khác nhau.\nĐiều này kéo theo hàng loạt bài toán systems mới:\nLàm thế nào để batch các request đang ở những bước decode khác nhau? Làm thế nào để scheduler liên tục đưa request mới vào GPU? Làm thế nào quản lý KV Cache khi sequence liên tục dài ra? Làm thế nào tối ưu đồng thời latency, throughput và chi phí GPU? \u0026hellip;và hàng chục câu hỏi khác. Từ giai đoạn LLM deployment bùng nổ, những câu hỏi này dần trở thành một hướng nghiên cứu hệ thống riêng. Một dấu mốc tiêu biểu là PagedAttention/vLLM, công bố tại SOSP 2023. Paper này nhìn LLM serving từ góc độ quản lý bộ nhớ: KV Cache có kích thước lớn, thay đổi động, dễ gây fragmentation — nên tác giả mượn ý tưởng paging của hệ điều hành để quản lý nó hiệu quả hơn.\nGộp lại, LLM serving không đơn giản là \u0026ldquo;chạy LLM nhanh hơn\u0026rdquo;. Đó là bài toán phối hợp giữa model architecture, inference algorithm, scheduling, memory management, GPU runtime và distributed systems.\nInference vs Training Training là quá trình điều chỉnh trọng số (weights) của model để nó học pattern từ dữ liệu — forward pass, tính loss, backward pass, cập nhật weights, lặp lại hàng triệu lần. Việc này thường làm một lần (hoặc định kỳ), offline. Có thể mất nhiều giờ, nhiều ngày, thậm chí nhiều tuần để train một model mà không có người dùng nào phải ngồi chờ kết quả từng bước. Mục tiêu chính là:\nLàm sao hoàn thành training với lượng compute và thời gian hợp lý để model đạt chất lượng mong muốn?\nInference thì ngược lại — dùng bộ trọng số đã cố định để tạo output cho input mới, không có backward pass, không cập nhật weights. Nhưng nó diễn ra liên tục, hàng triệu lần mỗi ngày, và có người dùng thật đang chờ phản hồi ngay trên màn hình:\nVì thế, nếu training quan tâm nhiều đến $\\text{FLOPs}$ (Floating Point Operations Per Second) và thời gian hội tụ, serving phải quan tâm thêm đến latency, throughput, memory, concurrency, GPU utilization và cost/request.\nMột cách hình dung đơn giản: training giống như xây dựng một nhà máy, serving giống như vận hành nhà máy đó mỗi ngày. Xây nhà máy chậm một chút có thể chấp nhận được. Nhưng khi nhà máy đã chạy production, mỗi phút downtime hoặc mỗi đơn hàng xử lý chậm đều có thể trở thành tiền.\nLLM Serving khác gì API Backend thông thường? Một backend API thông thường (ví dụ API tra cứu đơn hàng): nhận request → query database hoặc tính toán đơn giản → trả response trong vài chục mili-giây. Chi phí xử lý gần như không đổi giữa các request, và server không cần \u0026ldquo;nhớ\u0026rdquo; gì giữa các bước xử lý của cùng một request.\nVới LLM serving, có ít nhất 3 điểm khác biệt:\nOutput sinh ra từng token một. Một request có thể mất từ vài trăm mili-giây đến hàng chục giây, tùy độ dài output — và hiện chưa có phương pháp nào biết trước chính xác độ dài đó.\nMỗi request cần một vùng nhớ trạng thái riêng gọi là KV Cache (lưu lại \u0026ldquo;ngữ cảnh\u0026rdquo; model đã xử lý), và vùng nhớ này lớn và thay đổi kích thước liên tục trong lúc xử lý. Theo nhóm phát triển vLLM, nếu quản lý không hiệu quả, KV Cache sẽ bị lãng phí nghiêm trọng do phân mảnh (fragmentation) và trùng lặp dữ liệu — điều này trực tiếp giới hạn số request có thể xử lý đồng thời trên một GPU (Kwon et al., SOSP 2023).\nChi phí tính toán trên mỗi request không cố định — request sinh 20 token rẻ hơn nhiều so với request sinh 2000 token, và ta chỉ biết chi phí thật sau khi model dừng sinh kết quả.\nĐây là lý do một backend viết bằng Flask/FastAPI đơn giản không đủ để serving LLM ở quy mô production — cần một engine chuyên biệt (vLLM, TensorRT-LLM, SGLang\u0026hellip;) quản lý bộ nhớ và lịch trình xử lý.\nMột request LLM thực sự chạy như thế nào? Một pipeline đơn giản có thể hình dung như sau:\nTrong đó Prefill và Decode là hai phần quan trọng nhất.\nTokenization LLM không trực tiếp làm việc với string, mà được tokenizer chuyển thành một chuỗi token ID. Ví dụ \u0026ldquo;The dog sat on the couch\u0026rdquo; được chuyển thành [4, 22, 63, 7, 4, 143].\nToken không nhất thiết tương ứng đúng với một từ — một từ có thể là một token, nhiều token, hoặc chỉ một phần của token. Từ \u0026ldquo;unhappiness\u0026rdquo; có thể chia thành \u0026ldquo;un\u0026rdquo; + \u0026ldquo;happi\u0026rdquo; + \u0026ldquo;ness\u0026rdquo;. Cách tokenizer chia văn bản ảnh hưởng trực tiếp đến số lượng token, context length, thời gian inference và chi phí API — đây cũng là lý do cùng một câu nhưng dùng tokenizer khác nhau có thể ra số token khác nhau đáng kể.\nPrefill Sau khi tokenization xong, toàn bộ token của prompt đầu vào được đưa qua model cùng một lúc, song song, để tính ra biểu diễn Key-Value (KV) cho từng token trong prompt. Bước này nặng về tính toán vì phải nhân ma trận với toàn bộ prompt, và nó quyết định thời gian phải chờ trước khi thấy chữ đầu tiên xuất hiện trên màn hình.\nDecode Sau prefill, model bắt đầu sinh câu trả lời — nhưng theo kiểu autoregressive: lần lượt, từng token một, mỗi token mới được sinh dựa trên toàn bộ token trước đó (prompt + các token đã sinh), thông qua KV Cache đã lưu từ bước prefill.\nVí dụ nhập prompt \u0026ldquo;Thủ đô Việt Nam là\u0026rdquo;, model sinh: Token 1 → \u0026ldquo;Hà\u0026rdquo;, Token 2 → \u0026ldquo;Nội\u0026rdquo;, Token 3 → \u0026ldquo;.\u0026rdquo;.\nKhác với prefill, bước decode nặng về đọc bộ nhớ hơn là tính toán — vì với mỗi token mới, GPU phải đọc lại toàn bộ trọng số model và KV Cache từ bộ nhớ, trong khi lượng phép tính thực tế cho một token lại nhỏ (Zhong et al., DistServe, OSDI 2024). Đây là lý do prefill và decode có đặc tính rất khác nhau, và một số hệ thống serving hiện đại (như DistServe) tách hẳn hai giai đoạn này ra chạy trên GPU riêng để tránh chúng cản trở nhau.\nDetokenization Sau khi model sinh token ID (ví dụ [15496, 995, ...]), server phải chuyển chúng trở lại thành text (\u0026quot;Hello world...\u0026quot;). Với API streaming, server gửi token về client ngay khi chúng được sinh, nên người dùng thấy chữ hiện dần thay vì phải chờ model sinh xong toàn bộ câu trả lời.\nĐo hiệu suất serving: latency và throughput Muốn biết cách serving hiện tại có hiệu quả hay không, cần trả lời hai câu hỏi: một user phải chờ bao lâu (latency), và một server phục vụ được bao nhiêu user (throughput).\nTTFT — Time To First Token TTFT là thời gian từ khi request được gửi đến khi token đầu tiên xuất hiện.\nNếu ChatGPT mất 3 giây mới bắt đầu hiện chữ, cảm giác của người dùng sẽ rất khác so với việc chữ xuất hiện sau 300ms — dù tổng thời gian sinh câu trả lời cuối cùng có thể giống nhau.\nTPOT — Time Per Output Token Sau token đầu tiên, ta quan tâm tốc độ sinh các token tiếp theo.\nTPOT là thời gian trung bình giữa các token output — càng thấp thì streaming càng mượt.\nTotal Latency và Throughput Một cách đơn giản để hình dung tổng latency:\n$$\\text{Total Latency} \\approx \\text{TTFT} + \\text{Decode Time}$$Nếu có 100 output token và tốc độ decode trung bình khoảng 30ms/token:\n$$\\text{TTFT} = 500\\text{ms}, \\quad \\text{Decode} \\approx 100 \\times 30\\text{ms} = 3000\\text{ms}$$$$\\text{Total} \\approx 3.5\\text{s}$$Đây là mô hình hóa đơn giản; benchmark thực tế còn có queueing, scheduling và các overhead khác.\nNếu latency trả lời \u0026ldquo;một request mất bao lâu?\u0026rdquo;, thì throughput trả lời \u0026ldquo;cả hệ thống sinh được bao nhiêu token trong một khoảng thời gian?\u0026rdquo;. Ví dụ 3 user sinh lần lượt 100, 200, 300 token trong một giây:\n$$\\text{Throughput} = 600 \\text{ tokens/s}$$Đây là throughput của cả hệ thống, không có nghĩa một user đang nhìn thấy 600 tokens/s — một hệ thống có thể đạt 1000 tokens/s nhờ phục vụ 100 request đồng thời, trong khi một request riêng lẻ chỉ nhận khoảng 10 tokens/s. Đây là điểm rất dễ hiểu nhầm khi đọc benchmark: đừng chỉ nhìn một con số TPS, hãy hỏi thêm — throughput tính theo cái gì, bao nhiêu concurrent request, input/output length bao nhiêu, TTFT/TPOT bao nhiêu, chạy trên hardware nào.\nVà nếu phải chọn một thứ để đào sâu tiếp theo, đó sẽ là KV Cache — vì muốn hiểu PagedAttention, continuous batching, prefix caching, hay tại sao LLM serving lại ngốn VRAM khủng khiếp, gần như đều phải bắt đầu từ đây.\nKV Cache: trung tâm của mọi tối ưu Nhắc lại nhanh: với mỗi token, ở mỗi layer và mỗi attention head, model tính ra một cặp vector Key (K) và Value (V). KV Cache lưu lại toàn bộ các cặp K/V này để những bước decode sau không phải tính lại từ đầu. Nó tăng dần theo số token đã xử lý (prompt + output đã sinh), và độ dài cuối cùng không biết trước — đây là nguồn gốc của mọi vấn đề tối ưu bên dưới.\nPaged KV Cache Cách \u0026ldquo;ngây thơ\u0026rdquo; để lưu KV Cache là cấp cho mỗi request một vùng nhớ liên tục (contiguous) đủ lớn cho độ dài tối đa model hỗ trợ — giống việc đặt trước cả rạp chỉ vì không biết khách sẽ ở lại bao lâu. Vấn đề: đa số request không dùng hết vùng đã cấp, gây lãng phí (internal fragmentation); các request có độ dài khác nhau còn để lại những khoảng trống không đủ lớn để tái sử dụng (external fragmentation) — nhóm vLLM đo được rằng cách làm này khiến các hệ thống trước đó chỉ dùng hiệu quả 20,4%–38,2% bộ nhớ đã cấp phát (Kwon et al., SOSP 2023).\nPaged KV Cache mượn nguyên ý tưởng phân trang bộ nhớ ảo (virtual memory paging) từ hệ điều hành: chia KV Cache thành các block kích thước cố định (ví dụ 16 token/block), không cần nằm liên tục trong bộ nhớ vật lý. Mỗi request có một block table — bảng ánh xạ từ vị trí logic của token sang địa chỉ vật lý (block nào, ở đâu trong GPU memory) — tương tự page table trong hệ điều hành. Nhờ vậy, hệ thống chỉ cấp phát thêm block mới khi request thực sự cần, và có thể lấp các block trống bằng request khác — gần như xoá sạch tình trạng lãng phí nêu trên.\nPagedAttention Chia KV Cache thành các block rời rạc kéo theo một vấn đề kỹ thuật: phép tính attention (nhân Query với toàn bộ Key/Value đã lưu) thường giả định dữ liệu nằm liên tục trong bộ nhớ để tính hiệu quả. PagedAttention là thuật toán attention được viết lại để đọc và tính toán trực tiếp trên các block rời rạc đó, tra theo block table, mà không cần \u0026ldquo;dồn\u0026rdquo; (gather) chúng về một vùng liên tục trước khi tính — tránh chi phí sao chép dữ liệu không cần thiết (Kwon et al., SOSP 2023).\nMột lợi ích đi kèm đáng chú ý: vì các block được quản lý như những đơn vị độc lập có thể tham chiếu từ nhiều nơi (giống cơ chế copy-on-write trong hệ điều hành), nhiều chuỗi có thể chia sẻ chung các block KV Cache khi chúng có phần đầu giống nhau — ví dụ trong parallel sampling (sinh nhiều output khác nhau từ cùng một prompt) hay beam search. Thay vì mỗi output phải lưu một bản KV Cache riêng cho đoạn prompt chung, chúng có thể tham chiếu tới cùng các block đó, chỉ tách ra (copy) khi nội dung thực sự khác nhau. Đây chính là ý tưởng nền cho prefix caching.\nPrefix caching PagedAttention giải quyết bài toán chia sẻ KV Cache trong một request (nhiều output cùng prompt). Nhưng có một cơ hội lớn hơn: rất nhiều request khác nhau lại có phần đầu (prefix) giống hệt nhau — cùng một system prompt, cùng vài ví dụ few-shot, hoặc cùng lịch sử hội thoại trong một cuộc chat nhiều lượt.\nPrefix caching là kỹ thuật phát hiện và tái sử dụng KV Cache cho phần prefix trùng nhau giữa các request, tránh phải tính lại prefill cho phần đó. vLLM có tính năng \u0026ldquo;automatic prefix caching\u0026rdquo; dựa trên PagedAttention. Đi xa hơn, SGLang giới thiệu RadixAttention (Zheng et al., NeurIPS 2024, arXiv:2312.07104) — quản lý toàn bộ KV Cache đã từng tính trong một cấu trúc cây gọi là radix tree, cho phép server tự động tìm phần prefix dài nhất đã có sẵn trong cache cho bất kỳ request mới tới, không chỉ giới hạn trong một request hay một phiên hội thoại đơn lẻ — kể cả với workload phức tạp hơn hội thoại tuyến tính, như agent phân nhánh nhiều hướng suy luận từ cùng một điểm xuất phát.\nLợi ích thực tế lớn nhất rơi vào các workload có tỷ lệ trùng prefix cao: chatbot dùng chung system prompt dài, ứng dụng agent gọi nhiều lượt với phần ngữ cảnh chung, hay RAG với cùng một số đoạn tài liệu được nhồi lại nhiều lần. Với các workload đó, prefix caching có thể cắt giảm đáng kể cả TTFT và tổng lượng tính toán tiêu thụ.\nKV Cache quantization Một hướng khác, độc lập với hai hướng trên: giảm số byte cần dùng cho mỗi giá trị trong KV Cache, tức lượng tử hóa (quantization) nó — ví dụ từ FP16 (16-bit) xuống 4-bit hay 2-bit.\nNhóm nghiên cứu KIVI (Liu et al., ICML 2024, arXiv:2402.02750) khảo sát phân phối giá trị thực tế trong KV Cache của các LLM phổ biến, và phát hiện Key và Value có đặc tính khác nhau: Key nên được lượng tử hóa theo chiều channel (nhóm các phần tử theo channel rồi lượng tử hóa cùng nhau), còn Value nên lượng tử hóa theo chiều token. Dựa trên phát hiện này, họ đề xuất KIVI — thuật toán lượng tử hóa 2-bit không cần tinh chỉnh (tuning-free) cho KV Cache. Trên các model Llama-2, Falcon và Mistral, KIVI giữ được chất lượng gần tương đương bản gốc trong khi giảm 2.6 lần bộ nhớ đỉnh, cho phép tăng batch size tối đa lên 4 lần, và đạt throughput cao hơn 2.35–3.47 lần trên workload inference thực tế (Liu et al., ICML 2024).\nĐây là một hướng nghiên cứu đang phát triển rất nhanh (nhiều biến thể mới như KVQuant, RotateKV, AsymKV liên tục xuất hiện sau KIVI) — cho thấy vẫn là bài toán mở, chưa có giải pháp \u0026ldquo;đóng băng\u0026rdquo; nào được toàn ngành đồng thuận.\nKhi nào KV Cache trở thành bottleneck? Gộp lại các phần trên, KV Cache có xu hướng trở thành nút thắt cổ chai rõ rệt nhất khi:\nContext dài. Kích thước KV Cache tăng tuyến tính theo số token đã xử lý, nên các ứng dụng dùng context window lớn (tài liệu dài, lịch sử hội thoại dài, RAG nhồi nhiều đoạn) sẽ nhanh chóng chiếm hết bộ nhớ khả dụng, dù model vẫn còn \u0026ldquo;chỗ\u0026rdquo; về mặt tham số. Concurrency cao. Nhiều request chạy đồng thời nghĩa là nhiều KV Cache riêng biệt cùng tồn tại — dung lượng KV Cache khả dụng chính là thứ đặt giới hạn trên cho batch size, chứ không phải sức mạnh tính toán của GPU. Ít trùng lặp prefix. Nếu mọi request đều hoàn toàn khác nhau (không có system prompt chung, không phải hội thoại nhiều lượt), prefix caching gần như không giúp được gì — bottleneck quay lại thuần về dung lượng và băng thông bộ nhớ. Quản lý bộ nhớ kém. Ngay cả khi tổng dung lượng GPU đủ lớn, cách cấp phát ngây thơ (không paging) vẫn có thể khiến hệ thống \u0026ldquo;cạn bộ nhớ\u0026rdquo; sớm hơn nhiều so với lý thuyết. Ba kỹ thuật ở trên không loại trừ nhau — một hệ thống production hiện đại thường kết hợp cả ba: paging để tránh lãng phí bộ nhớ, prefix caching để tránh tính lại phần trùng, và quantization để giảm dung lượng mỗi giá trị lưu trữ — cộng với các kỹ thuật giảm kích thước KV Cache ngay từ kiến trúc model (như GQA/MQA).\nNhưng có đủ bộ nhớ không có nghĩa là bạn đang dùng nó tốt. Ngay cả khi KV Cache dư dả, một cách lên lịch (schedule) tồi vẫn có thể khiến GPU ngồi không phần lớn thời gian trong khi request vẫn xếp hàng dài. Phần tiếp theo đi sâu vào cơ chế giải quyết đúng vấn đề đó — cơ chế đứng sau gần như mọi engine serving LLM hiện đại.\nBatching và Scheduling Vì sao batching truyền thống không hoạt động tốt với LLM? Batching kiểu truyền thống — gom một nhóm request cố định, xử lý tới khi tất cả xong mới trả kết quả — gọi là static batching. Với các model có output cố định độ dài (ảnh, phân loại), cách này ổn vì mọi request trong batch tốn thời gian gần như nhau. Với LLM thì không: độ dài output của mỗi request khác nhau hoàn toàn, và không ai biết trước con số đó.\nĐây chính là hạn chế mà nhóm Orca chỉ ra ở các hệ thống serving generative model thời điểm 2022: hệ thống dùng lịch trình cứng nhắc không thể thay đổi batch hiện tại, khiến request đã xong sớm không thể trả kết quả ngay, và request mới phải chờ (Yu et al., OSDI 2022).\nContinuous Batching Thay vì lên lịch ở cấp độ request (đưa cả batch vào, đợi cả batch ra), continuous batching lên lịch lại ở cấp độ iteration — mỗi bước sinh một token. Ở mỗi iteration, scheduler được gọi lại và có toàn quyền định nghĩa batch mới: ai rời đi, ai được thêm vào.\nĐọc timeline trên theo đúng cách nó vận hành: batch bắt đầu với A, B, C; request D mới tới được thêm thẳng vào ngay khi có chỗ trống, không cần đợi ai xong. Khi A hoàn tất, nó rời batch ngay lập tức — client nhận kết quả tức thì, không phải chờ B hay C — và request E đang xếp hàng được đưa vào, lấp ngay vào khoảng trống GPU vừa giải phóng. Không có một \u0026ldquo;điểm trả kết quả đồng loạt\u0026rdquo; nào cả — mỗi request có vòng đời độc lập trên cùng một GPU đang chạy liên tục.\nVề mặt thuật ngữ, ba cách gọi sau mô tả cùng một cơ chế, chỉ khác góc nhìn: request-level scheduling là cách cũ (đơn vị lên lịch là request), iteration-level scheduling là thuật ngữ Orca dùng để mô tả cơ chế bên dưới (đơn vị lên lịch là một bước sinh token), còn continuous batching là tên ngành công nghiệp dùng để mô tả kết quả quan sát được từ bên ngoài.\nKết quả đo được trong paper gốc, kết hợp thêm kỹ thuật selective batching (chỉ áp dụng batching cho các phép toán phù hợp như nhân ma trận tuyến tính, còn attention xử lý riêng từng chuỗi vì độ dài khác nhau): throughput cao hơn NVIDIA FasterTransformer tới 36.9 lần ở cùng mức độ trễ, khi phục vụ GPT-3 175B (Yu et al., OSDI 2022).\nChunked Prefill Continuous batching giải quyết \u0026ldquo;ai được ở trong batch\u0026rdquo;. Nhưng nó mở ra một vấn đề mới: trong cùng một iteration, batch có thể chứa cả request đang decode lẫn request cần prefill cùng lúc — ví dụ ba request A, B, D đang decode (mỗi bước chỉ cần sinh một token), trong khi request C vừa tới, cần prefill toàn bộ prompt trước khi có thể bắt đầu decode.\nVấn đề: hai loại công việc này có đặc tính trái ngược nhau — prefill muốn xử lý một lượng lớn token cùng lúc để tận dụng compute, còn decode chỉ sinh vài token mới mỗi bước nhưng cần chạy liên tục, đều đặn để giữ TPOT thấp. Khi hai loại này trộn vào cùng một batch, chúng cạnh tranh trực tiếp tài nguyên GPU trong cùng một iteration — đây chính xác là vấn đề mà nhóm Sarathi-Serve đặt ra làm động lực nghiên cứu: việc trộn các iteration prefill và decode trong cùng một batch khiến việc đạt đồng thời cả throughput cao lẫn latency thấp trở nên khó khăn (Agrawal et al., OSDI 2024).\nGiải pháp: đừng để một prompt dài \u0026ldquo;chiếm trọn\u0026rdquo; một iteration. Thay vào đó, chia nhỏ phần prefill thành các chunk gần bằng nhau, xử lý xen kẽ với các bước decode đang chạy, thay vì dồn hết vào một lượt.\nPanel trên minh họa hậu quả của việc không chunk: khi prefill của C (10.000 token) được xử lý trong một lượt duy nhất, các request A, B, D đang decode buộc phải \u0026ldquo;đứng hình\u0026rdquo; suốt khoảng thời gian đó — TPOT của chúng tăng vọt, dù bản thân chúng không có vấn đề gì. Panel dưới là cách Sarathi-Serve xử lý: prefill C được chia thành các chunk nhỏ (near-equal size), mỗi iteration chỉ \u0026ldquo;chen\u0026rdquo; một chunk vào cùng với các bước decode đang chạy — gọi là stall-free scheduling, vì nó cho phép thêm request mới vào batch mà không làm gián đoạn các decode đang chạy (Agrawal et al., OSDI 2024). Kích thước chunk (chunk size / token budget) là một tham số cần điều chỉnh: chunk quá nhỏ làm tăng overhead do phải chạy nhiều iteration hơn cho cùng một prefill; chunk quá lớn quay lại vấn đề ban đầu — đây là công cụ để đánh đổi giữa throughput và latency, không phải một giá trị \u0026ldquo;đặt một lần rồi quên\u0026rdquo; (Agrawal et al., OSDI 2024).\nScheduling: server quyết định ai được chạy Continuous batching và chunked prefill chỉ là hai kỹ thuật cụ thể. Đứng sau chúng là một bài toán tổng quát hơn: ở mỗi iteration, scheduler của serving engine phải quyết định chính xác việc gì sẽ chạy tiếp theo.\nVới mỗi request đang chờ hoặc đang chạy, scheduler phải trả lời liên tục: có admit request mới vào batch không? Nếu có, xử lý phần prefill hay tiếp tục decode của nó? Có đủ KV Cache khả dụng để nhận thêm không? Nếu không đủ điều kiện, request đó reject hẳn hay tiếp tục giữ trong hàng đợi?\nĐiều quan trọng: scheduler không tối ưu một chỉ số duy nhất — nó phải cân bằng đồng thời TTFT, TPOT, throughput tổng, dung lượng KV Cache còn trống, và mức độ tận dụng GPU. Đây chính là bài toán goodput mà DistServe hình thức hóa — thông lượng tối đa đạt được trong khi vẫn đáp ứng ràng buộc độ trễ đã cam kết (Zhong et al., OSDI 2024).\nĐánh đổi giữa Latency và Throughput Có một đánh đổi nền tảng, độc lập với việc bạn dùng kỹ thuật scheduling nào tinh vi tới đâu:\nBatch size Latency mỗi request Throughput hệ thống Nhỏ (ví dụ = 1) Thấp — không phải chờ, không bị request khác cạnh tranh Thấp — GPU phần lớn thời gian rảnh, không tận dụng được song song Lớn (ví dụ = 100) Cao hơn — mỗi bước decode phải \u0026ldquo;chia sẻ\u0026rdquo; GPU với nhiều request hơn Cao — GPU bận rộn liên tục, tổng số token/giây tăng mạnh Orca và Iteration-Level Scheduling — một case study lịch sử Nhìn lại toàn cảnh: trước Orca, \u0026ldquo;chuẩn công nghiệp\u0026rdquo; cho serving generative model gần giống hệt static batching — nhận request, gom vào batch, chạy tới khi xong mới trả kết quả. Cách nghĩ này thực chất đang mượn nguyên tư duy serving các model có output cố định độ dài, áp lên một workload hoàn toàn khác bản chất.\nĐóng góp cốt lõi của Orca (Yu et al., OSDI 2022) không chỉ là một tối ưu kỹ thuật — nó là một thay đổi tư duy: scheduler không còn nghĩ theo đơn vị request, mà theo đơn vị iteration/bước sinh token. Một khi đã chấp nhận đơn vị lên lịch nhỏ hơn này, hàng loạt kỹ thuật khác — chunked prefill, prefix caching, prefill/decode disaggregation — đều trở thành các biến thể tinh chỉnh thêm trên cùng một nền tảng tư duy đó, chứ không phải những phát minh độc lập, rời rạc.\nChọn serving engine nào? Từ các phần trên, ta đã nói về continuous batching, PagedAttention, RadixAttention, chunked prefill, KV Cache quantization — toàn những kỹ thuật bên trong một serving engine. Nhưng thực tế, gần như không ai tự viết engine từ đầu nữa. Câu hỏi thực dụng hơn nhiều là: chọn engine nào?\nBốn cái tên xuất hiện thường xuyên nhất: vLLM, SGLang, NVIDIA TensorRT-LLM, và Hugging Face TGI (Text Generation Inference).\nvLLM là engine ra đời từ paper PagedAttention (Kwon et al., SOSP 2023), phát triển từ dự án nghiên cứu tại UC Berkeley. Theo README chính thức, vLLM hỗ trợ GPU NVIDIA, AMD, Intel, CPU x86/ARM/PowerPC, cùng nhiều plugin phần cứng khác (Google TPU, Intel Gaudi, IBM Spyre, Huawei Ascend, Rebellions NPU, Apple Silicon, MetaX GPU\u0026hellip;), và hỗ trợ hơn 200 kiến trúc model trên Hugging Face.\nSGLang là engine đứng sau RadixAttention (Zheng et al., NeurIPS 2024), hiện do tổ chức phi lợi nhuận LMSYS duy trì. Theo README chính thức, SGLang hỗ trợ GPU NVIDIA (bao gồm cả dòng GB200/B300 mới), AMD, Intel Xeon CPU, Google TPU, và Ascend NPU, với hơn 400.000 GPU đang chạy SGLang trong production trên toàn thế giới — được các tổ chức như xAI, NVIDIA, AMD, LinkedIn, Google Cloud, Microsoft Azure, AWS sử dụng. Điểm mạnh nổi bật nhất của SGLang nằm ở workload có nhiều phần đầu (prefix) trùng lặp — chatbot dùng chung system prompt, RAG lặp lại cùng tài liệu, agent nhiều lượt — nhờ RadixAttention tự động phát hiện và tái sử dụng KV Cache đã tính. SGLang cũng là engine được đội DeepSeek chính thức khuyến nghị để phục vụ các model DeepSeek (V3, R1\u0026hellip;) ngay từ ngày phát hành.\nTổng hợp lại thành khung quyết định thực dụng:\nTình huống Nên chọn Vì sao Không chắc chắn, cần lựa chọn \u0026ldquo;an toàn\u0026rdquo; vLLM Hỗ trợ phần cứng rộng nhất, cộng đồng lớn nhất, dễ deploy nhất Chatbot/agent/RAG dùng chung system prompt hoặc ngữ cảnh SGLang RadixAttention phát huy tối đa lợi thế đúng tình huống này Đang serving model họ DeepSeek SGLang Engine được chính đội DeepSeek khuyến nghị và tối ưu riêng (MLA, MoE routing) Hạ tầng toàn NVIDIA, cần vắt kiệt throughput cho model ổn định lâu dài, sẵn sàng đầu tư thời gian build/tune TensorRT-LLM Tối ưu sâu nhất cho hệ sinh thái NVIDIA Đang chạy TGI trong production Giữ nguyên, lên kế hoạch migrate TGI không còn nhận tính năng mới hay hỗ trợ kiến trúc model mới từ cuối 2025 Bên cạnh bốn cái tên chính, thị trường 2026 xuất hiện thêm vài dự án được nhắc tới như \u0026ldquo;ứng viên thứ năm\u0026rdquo; — ví dụ Modular MAX (biên dịch kernel bằng ngôn ngữ Mojo, được quảng cáo vượt vLLM trên một số model dense ở tải cao) hay Aphrodite Engine (hướng tới hỗ trợ checkpoint dạng cộng đồng như GGUF/EXL2 và các sampler nâng cao). Chưa có đủ dữ liệu benchmark độc lập, đa nguồn để đánh giá các dự án này ở mức chi tiết như bốn cái tên trên — nên xem đây là gợi ý để theo dõi thêm, không phải khuyến nghị.\nKết luận Serving một LLM production-grade không phải là \u0026ldquo;chạy model nhanh hơn\u0026rdquo; — đó là bài toán hệ thống nhiều lớp: hiểu prefill/decode có đặc tính trái ngược nhau, quản lý KV Cache sao cho không lãng phí VRAM, lên lịch ở cấp độ iteration thay vì request, và biết đánh đổi giữa latency và throughput tùy workload. Một model 70B benchmark đẹp trên máy cá nhân không nói lên điều gì về việc nó có phục vụ nổi 5000 request đồng thời hay không — câu trả lời nằm ở engine serving đứng sau nó, chứ không nằm ở bản thân model.\n","permalink":"https://vo-hoang-kh4ng.github.io/posts/serving-llm/","summary":"Model benchmark đẹp, demo trên máy cá nhân mượt — nhưng đưa lên production cho 5000 người dùng cùng lúc thì GPU báo Out-of-memory. Đi từ prefill/decode, KV Cache, PagedAttention, continuous batching, chunked prefill, đến cách chọn serving engine (vLLM, SGLang\u0026hellip;) — và vì sao vấn đề không nằm ở model mà nằm ở cách serving.","title":"Serving LLM: Vì sao chạy production khó hơn demo rất nhiều"},{"content":"Trong thời đại bùng nổ nhu cầu triển khai chatbot hay trợ lý AI của doanh nghiệp, gần như tất yếu với các công ty có dữ liệu nhạy cảm hoặc ngành đặc thù cần kiến thức domain sâu, việc dùng một general model (GPT, Gemini,\u0026hellip;) thường chưa đủ.\nCác model general có thể làm rất giỏi các việc hằng ngày như tóm tắt, dịch, hỏi đáp hay suy luận, nhưng không biết doanh nghiệp thật sự đang làm gì. Để giải quyết bài toán này, hiện tại có 3 phương pháp chính: Prompt Engineering, RAG và Fine-tuning.\nBài này đi theo đúng mạch đó: điểm qua Prompt Engineering và RAG để thấy vì sao đôi khi phải chọn Fine-tuning, sau đó vào cách chuẩn bị dữ liệu training cho đúng, rồi cuối cùng là các kỹ thuật fine-tuning tiết kiệm tài nguyên — từ Full Fine-tuning đến LoRA, QLoRA và DoRA — cùng lý do từng kỹ thuật ra đời để giải quyết giới hạn của kỹ thuật trước nó.\nPrompt Engineering Nhét hướng dẫn của user vào system prompt hiện tại, không thay đổi trọng số của model.\nƯu điểm: nhanh, rẻ, dễ thử nghiệm, không cần retrain model \u0026amp; thu thập dataset. Nhược điểm: tràn context, tốn token, khó quản lý. Note: Token là đơn vị nhỏ nhất mà LLM xử lý — có thể là 1 từ, 1 phần từ, hoặc 1 dấu câu.\nRetrieval-Augmented Generation (RAG) Cách phổ biến khi doanh nghiệp triển khai chatbot trên tài liệu nội bộ. Thay vì cố nhồi nhét một đống tài liệu vào prompt, flow của RAG như sau:\nCâu hỏi → tìm tài liệu liên quan → lấy chính sách phù hợp → đưa cho LLM → LLM tạo câu trả lời.\nĐiểm hay là khi tài liệu/chính sách thay đổi, doanh nghiệp chỉ cần cập nhật tài liệu thay vì retrain model. Nhược điểm nằm ở việc nếu chunk tài liệu không tốt thì LLM sẽ đưa kết quả sai.\nFine-tuning Nếu Prompt là hướng dẫn, RAG là cho AI tài liệu để đọc, thì Fine-tuning gần giống như đào tạo AI thành một người đã quen với công việc — không cần tra cứu, không cần ai hướng dẫn.\nTiêu chí tối ưu hóa Prompt Engineering RAG Fine-Tuning Bản chất can thiệp Điều chỉnh đầu vào (input) Cung cấp ngữ cảnh thời gian thực Cập nhật tham số mô hình (weights) Chi phí / tài nguyên Rất thấp (không cần hạ tầng riêng) Trung bình (vector DB, pipeline) Rất cao (GPU, chuyên gia MLOps) Khả năng cập nhật Tức thì, đổi lời nhắc Linh hoạt qua database Phức tạp, cần train và đánh giá lại Mục tiêu ưu tiên Thiết lập định dạng, kiểm tra nhanh Độ chính xác thông tin, chống ảo giác Chuyên môn hóa hành vi, đồng bộ giọng điệu Độ trễ vận hành Phụ thuộc độ dài prompt Cao (cộng thêm thời gian truy xuất) Thấp nhất (nếu model nhỏ, prompt ngắn) Chuẩn bị dữ liệu để training Trước khi bàn tới các kỹ thuật fine-tuning, cần trả lời 3 câu hỏi về dữ liệu: cần loại data nào, cần bao nhiêu, và data phải trông như thế nào — vì kỹ thuật fine-tuning giỏi đến đâu cũng không cứu được một dataset chuẩn bị sai.\nFine-tuning cần loại data nào? Model tốt đến đâu cũng khó cứu được một dataset tệ.\nNếu đưa cho model một tỷ ví dụ nhưng rất nhiều câu trả lời sai, format lộn xộn, model sẽ học luôn các thứ sai đó. Vì vậy trong Fine-tuning, chất lượng dữ liệu thường quan trọng không kém số lượng dữ liệu.\nBao nhiêu data là đủ? Không có con số cố định cho mọi bài toán.\nMục tiêu là thay đổi cách model trả lời → vài trăm đến vài nghìn sample là đủ thấy hiệu quả. Mục tiêu là để model quen với domain cụ thể (pháp lý, y khoa,\u0026hellip;) → thường cần từ vài nghìn đến vài trăm nghìn sample. Nhưng chốt lại: 1.000 mẫu tốt có thể có giá trị hơn 50.000 mẫu rác.\nData fine-tuning trông như thế nào? Dữ liệu thường phải chuẩn hóa thành một format mà framework/model có thể hiểu. Có 2 format phổ biến: Alpaca-style và Chat/ShareGPT-style.\nAlpaca — cách biểu diễn đơn giản, mỗi sample như một bài tập:\nInstruction: muốn model làm gì? Input: dữ liệu đầu vào là gì? Output: câu trả lời mong muốn là gì? Ví dụ:\n{ \u0026#34;instruction\u0026#34;: \u0026#34;Tóm tắt đoạn văn bản sau bằng một câu.\u0026#34;, \u0026#34;input\u0026#34;: \u0026#34;Hàng triệu năm trước, khủng long cai trị trái đất trước khi một thảm họa thiên thạch xảy ra...\u0026#34;, \u0026#34;output\u0026#34;: \u0026#34;Khủng long từng thống trị trái đất trước khi tuyệt chủng do thảm họa thiên thạch.\u0026#34; } Phù hợp cho các task dạng input → output, ví dụ tóm tắt văn bản.\nChat/ShareGPT — LLM ngày nay không chỉ làm bài toán input-output, các chatbot phải trò chuyện qua nhiều turn:\nSystem: Bạn là trợ lý chăm sóc khách hàng. User: Internet nhà tôi bị chậm. Assistant: Anh/chị có thể kiểm tra giúp em... User: Tôi đã khởi động lại modem rồi. Và assistant phải hiểu câu sau liên quan đến hội thoại trước. Với bài toán như vậy, format Chat/ShareGPT phù hợp hơn:\n{ \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Bạn là trợ lý chăm sóc khách hàng viễn thông.\u0026#34; }, { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Internet nhà tôi bị chậm.\u0026#34; }, { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Anh/chị có thể kiểm tra giúp em đèn Internet trên modem đang sáng hay đỏ không ạ?\u0026#34; } ] } Mỗi message có một role:\nsystem: quy định AI phải hoạt động như thế nào. user: người dùng đang nói gì. assistant: câu trả lời mà model cần học. Model không học từng câu riêng lẻ mà học cả một cuộc hội thoại diễn ra.\nChọn đúng format là một chuyện, nhưng khi ghép hàng loạt sample có độ dài khác nhau vào một batch để train, còn một vấn đề khác đang âm thầm ngốn compute: padding.\nPadding Tax Có cuộc hội thoại chỉ dài 100 token, có cuộc lại dài tới 3.000 token. Chuẩn hóa độ dài này như nào cho hợp lý?\nGiả sử có 3 câu: A = 50 token, B = 100 token, C = 200 token. Khi đưa vào cùng một batch, Transformer thường cần các chuỗi có cùng kích thước, nên hệ thống lấy câu dài nhất làm chuẩn — cả A và B đều bị [PAD] thêm cho đủ 200 token.\nKết quả: thay vì xử lý 50 + 100 + 200 = 350 token, GPU phải xử lý 200 × 3 = 600 token. Đây chính là thứ người ta gọi vui là Padding Tax.\nVấn đề nghiêm trọng hơn khi train LLM, vì Attention có chi phí tăng rất nhanh theo độ dài sequence: sequence càng dài → tính toán càng tăng mạnh. Vậy nên dành nhiều compute chỉ để xử lý [PAD] là một sự lãng phí.\nNote: Attention là cơ chế giúp mỗi token \u0026ldquo;nhìn\u0026rdquo; vào các token khác trong câu để hiểu ngữ cảnh.\nDynamic Batching Ý tưởng: đừng xếp một câu 50 token chung với câu 2.000 token. Thay vào đó, gom từng câu có độ dài ngang nhau vào cùng batch — ví dụ batch gồm các câu 50-70 token, batch khác gồm các câu 850-1000 token.\nCách này dễ triển khai và ít padding hơn, nhưng padding vẫn tồn tại. Nếu dataset có độ dài rất khác nhau, GPU vẫn xử lý khá nhiều padding thừa.\nSequence Packing Thay vì cố làm mọi sample dài bằng nhau, ta làm ngược lại: ghép nhiều sample ngắn vào cùng một sequence dài.\nVí dụ context window 1.000 token, với Sample A (300 token), B (400 token), C (250 token): thay vì mỗi sample kèm hàng trăm token PAD, ta ghép chúng lại thành A | B | C = 950 token, chỉ còn 50 token chưa dùng.\nPhương pháp này hiệu quả trên dataset có nhiều sample ngắn, nhưng có một hạn chế cố hữu: nếu đơn giản nối A + B thành một sequence, Transformer có thể nhìn thấy toàn bộ sequence — token trong Sample A có khả năng \u0026ldquo;nhìn thấy\u0026rdquo; token trong Sample B thông qua Attention. Đây là điều không mong muốn, vì hai sample hoàn toàn độc lập về mặt logic. Hiện tượng này gọi là Information Contamination.\nKhông thể cứ nối tất cả sample lại với nhau rồi train như một sequence bình thường.\nBlock-Diagonal Attention Giải pháp là dùng Attention Mask: dựng một \u0026ldquo;bức tường\u0026rdquo; giữa các sample trong cùng sequence.\nToken trong Sample A chỉ được Attention với token của A, token trong Sample B chỉ được Attention với token của B. Hai sample dùng chung một sequence vật lý nhưng về mặt logic vẫn hoàn toàn độc lập.\nCòn Position thì sao? Nếu để position chạy liên tục (Sample A: 0→299, Sample B: 300→699), model có thể hiểu nhầm Sample B là phần tiếp nối của Sample A trong cùng một câu. Vì vậy, position index cũng được reset lại từ 0 cho mỗi sample, để mỗi sample vẫn giữ được \u0026ldquo;không gian vị trí\u0026rdquo; riêng.\nVSL + Flash Attention Thay vì tạo một tensor đầy [PAD], hệ thống có thể flatten toàn bộ token thành một chuỗi liên tục, đồng thời lưu thông tin điểm bắt đầu/kết thúc của từng sample (biểu diễn bằng cumulative sequence lengths).\nNote: Flash Attention là kỹ thuật tính attention nhanh hơn và tốn ít bộ nhớ hơn cách tính thông thường, bằng cách xử lý theo từng khối nhỏ thay vì toàn bộ ma trận cùng lúc.\nFlash Attention sau đó biết: \u0026ldquo;đoạn 0–300 là sample A, đoạn 300–700 là sample B, đoạn 700–950 là sample C.\u0026rdquo;\nĐến đây, ta đã giải quyết một phần bài toán Fine-tuning:\nDataset phải đủ tốt. Dữ liệu phải có format phù hợp. Batch cần được đóng gói hiệu quả. Padding cần được hạn chế để GPU không lãng phí compute. Coi như xong phần chuẩn bị dữ liệu. Phần tiếp theo mới là trọng tâm của bài: các phương pháp fine-tuning phổ biến, bắt đầu từ cách truyền thống nhất rồi đi dần tới các kỹ thuật tiết kiệm tài nguyên hơn.\nFull Fine-tuning Cách truyền thống là Full Fine-Tuning (FFT): model có bao nhiêu tham số thì training cập nhật bấy nhiêu tham số.\nVí dụ một model 7 tỷ parameters, cả 7 tỷ đều có thể được cập nhật qua backpropagation.\nNote: Backpropagation là quá trình model tự điều chỉnh trọng số sau mỗi lần đoán sai, bằng cách lan truyền ngược lỗi từ output về từng tham số.\nVấn đề: 7 tỷ tham số không chỉ tốn bộ nhớ để lưu weights. Trong quá trình training, GPU còn phải lưu thêm gradients, optimizer states, activation, và nhiều buffer trung gian khác. Vì vậy một model \u0026ldquo;chỉ 7B\u0026rdquo; có thể cần lượng VRAM lớn hơn rất nhiều so với việc chỉ chạy inference.\nChạy một LLM và Fine-tune một LLM là hai câu chuyện hoàn toàn khác nhau về yêu cầu phần cứng.\nVới các model hàng chục hoặc hàng trăm tỷ tham số, Full Fine-tuning nhanh chóng trở thành bài toán hạ tầng rất đắt đỏ. Thay vì thay đổi cả model, tại sao không chỉ thay đổi một phần rất nhỏ? Đó chính là ý tưởng của PEFT.\nParameter-Efficient Fine-Tuning (PEFT) Ý tưởng rất đơn giản: đóng băng model gốc và chỉ train thêm một lượng nhỏ parameters.\nHãy tưởng tượng base model là một nhân viên đã có rất nhiều kiến thức. Nếu muốn nhân viên này làm thêm một công việc mới, không nhất thiết phải \u0026ldquo;cho học lại toàn bộ kiến thức từ đầu\u0026rdquo; — có thể giữ nguyên kiến thức cũ và chỉ đào tạo thêm một module chuyên môn.\nĐây là lý do PEFT tiết kiệm đáng kể VRAM, compute, thời gian training, storage và chi phí triển khai. Trong số các phương pháp PEFT, cái tên nổi bật nhất là LoRA – Low-Rank Adaptation.\nLoRA Hãy tưởng tượng một layer của model có một ma trận trọng số cực kỳ lớn. Trong Full Fine-tuning, ta học toàn bộ thay đổi $W \\to W + \\Delta W$, tức model phải học một ma trận $\\Delta W$ khổng lồ.\nLoRA đặt câu hỏi: liệu có cần học toàn bộ $\\Delta W$ không? Câu trả lời là không nhất thiết. LoRA giả định phần thay đổi cần thiết cho một task cụ thể có thể được biểu diễn bằng một không gian nhỏ hơn rất nhiều. Thay vì học trực tiếp $\\Delta W$, LoRA tách nó thành hai ma trận nhỏ:\n$$\\Delta W \\approx B \\cdot A$$$$W_{new} = W + B \\cdot A$$với rank $r$ nhỏ hơn rất nhiều so với kích thước ma trận ban đầu.\nNhưng paper LoRA gốc (Hu et al., 2021, arXiv:2106.09685) đặt bài toán tổng quát hơn:\nvới $\\Phi$ được khởi tạo từ $\\Phi_0$ (pretrained) và cập nhật thành $\\Phi_0 + \\Delta\\Phi$, nhưng $|\\Delta\\Phi| = |\\Phi_0|$ — đây chính là lý do FFT tốn kém: GPT-3 có $|\\Phi_0| \\approx 175$ tỷ thì $\\Delta\\Phi$ cũng lớn tương đương.\nLoRA thay bằng: mã hóa $\\Delta\\Phi = \\Delta\\Phi(\\Theta)$ qua một tập tham số $\\Theta$ nhỏ hơn nhiều ($|\\Theta| \\ll |\\Phi_0|$), bài toán tối ưu trở thành:\nVới GPT-3 175B, $|\\Theta|$ có thể nhỏ tới 0.01% của $|\\Phi_0|$ — đây chính là ý nghĩa của cụm \u0026ldquo;adapter nhỏ\u0026rdquo; sẽ được nhắc lại xuyên suốt phần sau.\nTrước LoRA, người ta đã thử gì? LoRA không phải ý tưởng PEFT đầu tiên. Trước đó có hai hướng chính, cả hai đều có vấn đề khiến LoRA ra đời.\nAdapter layer — chèn thêm vài lớp nhỏ xen giữa các layer gốc của Transformer. Vấn đề: các lớp này phải chạy tuần tự, không thể song song hóa. Dù adapter rất nhẹ (đôi khi \u0026lt;1% tham số), việc bắt GPU chạy thêm một bước tính toán nối tiếp vẫn cộng dồn thành latency thật khi inference (đo trên GPT-2 medium với batch size = 1, tình huống inference online phục vụ từng request một).\nPrefix-tuning — không đụng vào weight, mà tối ưu trực tiếp một số token đặc biệt chèn vào đầu input. Vấn đề: khó optimize (hiệu năng không tăng đều khi tăng số token), và các token này chiếm luôn một phần context window vốn dành cho nội dung thật của task.\nĐây là lý do LoRA chọn thiết kế khác hẳn: cộng song song vào weight gốc ($W + BA$) thay vì chèn nối tiếp như Adapter, và sau khi train xong có thể merge thẳng $BA$ vào $W$ — nên không tốn thêm latency lúc inference, không đụng vào context window lúc train.\nNói dân dã:\nFull Fine-tuning: sửa cả bức tường. LoRA: giữ nguyên bức tường, chỉ gắn thêm một tấm bảng nhỏ lên đó.\nTấm bảng này chính là LoRA adapter.\nNhìn hình trên dễ thắc mắc: tại sao khởi tạo $A \\sim \\mathcal{N}(0, \\sigma^2)$, $B = 0$? Tại sao không phải ngược lại? Tại sao không phải cả 2 bằng 0?\nĐây không phải lựa chọn tùy ý:\nNếu cả $A$ và $B$ đều $= 0$ → gradient của $A$ cũng bằng 0 tại bước đầu (vì $\\partial L / \\partial A$ phụ thuộc vào $B$), model \u0026ldquo;chết\u0026rdquo;, không học được gì. Nếu cả $A$ và $B$ đều random → $\\Delta W = BA \\neq 0$ ngay từ đầu → model xuất phát từ một điểm lệch khỏi pretrained weights một cách ngẫu nhiên, có thể phá vỡ hành vi đã học trước khi adapter kịp học điều gì có ích. Chọn $B = 0$, $A$ random → $\\Delta W = BA = 0$ chính xác tại bước 0 (vì bất kỳ số nào nhân với ma trận 0 đều ra 0), nhưng gradient của $B$ vẫn khác 0 (vì phụ thuộc vào $A$ đã có giá trị random) → training bắt đầu, mà model vẫn xuất phát y hệt pretrained. Nói cách khác: $B = 0$ đảm bảo \u0026ldquo;điểm xuất phát an toàn\u0026rdquo;, còn $A$ random đảm bảo \u0026ldquo;có tín hiệu để học\u0026rdquo;.\nVì sao một adapter nhỏ lại đủ? Một LLM có thể có hàng tỷ tham số, nhưng khi chuyển từ task này sang task khác, không phải toàn bộ kiến thức của model đều cần thay đổi. Ví dụ base model đã biết tiếng Việt, tiếng Anh, kiến thức phổ thông, cách lập luận, cách viết câu, kiến thức lập trình. Nếu muốn nó trở thành \u0026ldquo;trợ lý phân loại cuộc gọi khách hàng viễn thông\u0026rdquo;, không cần dạy lại toàn bộ những thứ trên — chỉ cần model học thêm những pattern đặc thù của task.\nLoRA cố gắng lưu phần thay đổi đó vào một tập tham số nhỏ hơn rất nhiều. Kết quả: thay vì lưu một bản model hàng chục GB cho mỗi task, ta có thể giữ một base model duy nhất kết hợp với nhiều adapter khác nhau.\nMột hyperparameter quan trọng của LoRA là rank r — mức độ \u0026ldquo;dung lượng\u0026rdquo; mà adapter được phép sử dụng để học task mới.\nr nhỏ → adapter nhỏ → ít tham số → train nhanh hơn → nhưng khả năng biểu diễn hạn chế hơn r lớn → adapter lớn → nhiều tham số hơn → học pattern phức tạp hơn → nhưng tốn compute và memory hơn Ví dụ thường gặp: r = 8, 16, 32, 64. Không có một giá trị r đúng cho mọi bài toán — task đơn giản thì rank nhỏ có thể đã đủ, task phức tạp hoặc data lớn có thể cần rank cao hơn.\nRank là một trade-off giữa năng lực biểu diễn và chi phí.\nHyperparameter khác là alpha ($\\alpha$) — nếu rank quyết định adapter \u0026ldquo;học được bao nhiêu\u0026rdquo;, alpha là mức độ ảnh hưởng của adapter lên model gốc:\n$$W_{new} = W + \\frac{\\alpha}{r} \\cdot BA$$$\\alpha/r$ quyết định mức scale của phần cập nhật LoRA. Ví dụ $r = 16, \\alpha = 32 \\Rightarrow \\alpha/r = 2$.\nPaper LoRA gốc mô tả: sau khi chọn một giá trị $r$ để thử nghiệm đầu tiên, họ set α bằng đúng con số r đó, rồi giữ nguyên α không đổi kể cả khi thử các giá trị $r$ khác nhau — tức α là hằng số cố định, không tự động scale theo r mỗi lần đổi rank. Lý do: khi optimize bằng Adam, việc tune α gần tương đương tune learning rate (cả hai đều chỉ scale độ lớn bước cập nhật), nên thay vì grid-search cả α lẫn r cùng lúc, họ cố định α để giảm số hyperparameter cần tune.\nNhưng các blog/tutorial khác (từ 2023 trở đi) lại đi ngược lại với LoRA gốc: cộng đồng dùng rule of thumb $\\alpha = 2r$ — để tỷ lệ $\\alpha/r$ cố định (=2) mỗi khi đổi r, chứ không giữ α cố định như paper gốc. Đây không phải sai lầm ngẫu nhiên — hai paper sau này (Kalajdzievski 2023, và Biderman et al. 2024 trong \u0026ldquo;LoRA vs Full Fine-tuning: An Illusion of Equivalence\u0026rdquo;) chỉ ra: nếu giữ α cố định trong khi tăng r, effective rank của update matrix bị \u0026ldquo;sụp\u0026rdquo; xuống mức thấp hơn nhiều so với r cho phép — tăng r mà không tăng α theo tỷ lệ sẽ lãng phí, model không tận dụng được rank cao hơn đã trả tiền cho nó.\nGắn LoRA vào đâu? Transformer có rất nhiều layer, không nhất thiết LoRA phải gắn vào tất cả. Có thể chỉ định những module muốn train, chẳng hạn các projection trong Attention (q_proj, k_proj, v_proj, o_proj), hoặc mở rộng sang nhiều linear layer hơn.\nGắn ít → nhẹ hơn nhưng có thể hạn chế khả năng học. Gắn nhiều → nhiều tham số hơn nhưng có thể đạt hiệu quả tốt hơn. Khi cấu hình LoRA, ta thường cân nhắc ít nhất ba thứ: rank (adapter lớn đến đâu), alpha (adapter ảnh hưởng mạnh đến đâu), và target modules (adapter gắn vào đâu).\nCó một cách nhìn gọn để hiểu vì sao LoRA \u0026ldquo;gần\u0026rdquo; với FFT hơn Adapter hay Prefix-tuning: nếu áp LoRA vào toàn bộ các ma trận trọng số và cho phép r lớn bằng đúng rank thật của ma trận pretrained, về mặt lý thuyết LoRA sẽ hội tụ về chính xác Full Fine-tuning. Trong khi đó, càng tăng tham số, Adapter chỉ hội tụ về một MLP giới hạn, còn Prefix-tuning bị chặn bởi độ dài context có thể dùng.\nNhưng vẫn còn một thứ chiếm rất nhiều VRAM: base model. Trong LoRA, phần lớn trọng số model gốc vẫn được giữ nguyên và đóng băng — nhưng \u0026ldquo;không train\u0026rdquo; không có nghĩa \u0026ldquo;không cần lưu\u0026rdquo;. Nếu model có hàng chục tỷ tham số, vẫn phải đưa toàn bộ trọng số đó vào GPU để thực hiện forward pass.\nCâu hỏi tiếp theo rất tự nhiên: nếu không cần cập nhật các trọng số này, liệu có cần lưu chúng ở độ chính xác cao như vậy không? Câu trả lời: không nhất thiết. Đây chính là ý tưởng đằng sau QLoRA – Quantized LoRA.\nQLoRA QLoRA (Dettmers et al., NeurIPS 2023, arXiv:2305.14314) đặt lại câu hỏi ở phần trước. Thay vì lưu base model ở FP16 hoặc BF16, QLoRA lượng tử hóa (quantize) trọng số của model xuống 4-bit. Trong khi đó, phần LoRA adapter vẫn được train ở độ chính xác cao hơn, thường là FP16/BF16.\nBase Model → quantize xuống 4-bit → giữ nguyên, không train LoRA Adapter → FP16 / BF16 → train bình thường Ý tưởng khá đẹp: không cần giảm độ chính xác của thứ đang học nhiều, chỉ cần giảm độ chính xác của phần đang được đóng băng.\nLiệu 4-bit có làm model \u0026ldquo;kém\u0026rdquo; đi không? Về mặt logic, nhiều người sẽ nghĩ \u0026ldquo;từ 16-bit xuống 4-bit thì mất 75% thông tin, model chắc phải kém đi rất nhiều?\u0026rdquo; Nhưng không đơn giản như vậy — quantize không phải tùy tiện vứt bỏ bit, mà tìm cách biểu diễn trọng số bằng ít bit hơn nhưng giữ lại thông tin quan trọng nhất. QLoRA đưa ra một số kỹ thuật để làm việc này hiệu quả hơn, điển hình là NF4.\nNF4 – 4-bit Một thành phần quan trọng của QLoRA là NormalFloat4 (NF4), dựa trên fact: \u0026ldquo;trọng số của neural network thường có phân phối gần với phân phối chuẩn\u0026rdquo;. Có 3 cách giải thích cho việc này:\n(1) Khởi tạo. Ngay từ đầu, weights thường được lấy từ Gaussian — các cách khởi tạo phổ biến như Xavier/Glorot và He initialization thường lấy weight từ một phân phối đối xứng quanh 0, thường là Gaussian hoặc uniform với variance được kiểm soát.\nMục đích không phải để đẹp, mà tránh activation và gradient bị quá lớn dẫn tới exploding, hoặc quá nhỏ dẫn tới vanishing.\n(2) Neural network có rất nhiều phép cộng. Một neuron thường tính tổng của trọng số nhân với đặc trưng:\n$$z = \\sum_{i=1}^{n} w_i x_i$$Theo định lý giới hạn trung tâm (Central Limit Theorem), dưới một số điều kiện nhất định, tổng của một số lượng lớn các biến ngẫu nhiên độc lập sẽ có phân phối tiến gần phân phối chuẩn sau khi chuẩn hóa.\n(3) Training thường không phá vỡ hoàn toàn cấu trúc. Trong quá trình backpropagation:\n$$W_{new} = W - \\eta \\nabla W$$weights liên tục bị điều chỉnh, nhưng với các optimizer và regularization thông thường, phần lớn weights vẫn quanh một vùng trung tâm — distribution vẫn có hình dạng tương đối \u0026ldquo;chuông\u0026rdquo;, dù không nhất thiết là Gaussian chuẩn.\nQuay lại NF4: nếu weights xấp xỉ phân phối chuẩn, hãy chọn 16 giá trị đại diện sao cho phù hợp với phân phối đó, thay vì chia khoảng giá trị thành các đoạn bằng nhau. Phần lớn weights nằm gần 0 → cần biểu diễn vùng này chi tiết hơn. Các giá trị cực lớn/nhỏ xuất hiện ít → không cần dành quá nhiều mức lượng tử hóa cho chúng.\nKhi lượng tử hóa trọng số, ta cần thêm một số quantization constants/scales để biết cách chuyển đổi giữa giá trị gốc và giá trị đã lượng tử hóa — nhưng các constants này cũng chiếm bộ nhớ. QLoRA đặt câu hỏi: nếu đã tiết kiệm bộ nhớ cho weights, tại sao không tiết kiệm tiếp ở phần metadata này? Đó chính là Double Quantization.\nDouble Quantization Thông thường ta chỉ quantize weights một lần. QLoRA quantize thêm cả quantization constants:\nWeights → quantize Quantization Constants → quantize tiếp Ví dụ, giả sử chia weights thành các block 64 phần tử. Với mỗi block, cần một scale/quantization constant để đưa các giá trị 4-bit trở lại gần giá trị gốc. Nếu scale được lưu bằng FP32, riêng scale đã tốn 32 bits / 64 weights = 0.5 bits/parameter. Nghe có vẻ nhỏ, nhưng với hàng chục tỷ parameters thì 0.5 bit/parameter trở thành một lượng bộ nhớ đáng kể.\nQLoRA nhận ra các scale này lại là một tập số có thể quantize lần nữa — thay vì scale bằng FP32, quantize xuống 8-bit scales. Nhưng lại xuất hiện vấn đề nhỏ: muốn giải mã 8-bit scales thì cũng cần một scale để giải mã chúng. Vì vậy ta có một cấu trúc hai tầng:\nTheo QLoRA, với block size 64 ở tầng đầu và lượng tử hóa constants ở tầng thứ hai, overhead của quantization constants giảm từ 0.5 bits/parameter xuống khoảng 0.127 bits/parameter, tiết kiệm khoảng 0.373 bits/parameter.\nPaged Optimizer Dù đã giảm đáng kể memory của weights và quantization constants, quá trình training vẫn còn vấn đề khác: memory không phải lúc nào cũng ổn định. Trong quá trình forward → backward → optimizer update, GPU memory có thể xuất hiện những spike tạm thời. Với các model lớn, chỉ cần một peak vượt VRAM khả dụng cũng có thể khiến training gặp Out-of-Memory (OOM).\nĐây là vấn đề mà Paged Optimizer của QLoRA giải quyết — tận dụng CUDA Unified Memory để cho phép memory được quản lý giữa GPU và CPU. Khi GPU không đủ memory, các page memory có thể được chuyển từ GPU sang CPU thay vì lập tức gây OOM.\nĐến đây, QLoRA đã giải quyết một trong những nút thắt lớn nhất của Fine-Tuning LLM: memory. Thay vì lưu toàn bộ pretrained weights ở FP16, QLoRA đưa chúng xuống 4-bit bằng NF4, tiếp tục nén quantization constants bằng Double Quantization, đồng thời dùng Paged Optimizer để xử lý memory spike trong training.\nQLoRA giúp train một model lớn với ít memory hơn, nhưng bản chất của adapter vẫn là LoRA. Và LoRA vẫn tồn tại một vấn đề: hiệu năng đôi khi còn khoảng cách so với Full Fine-Tuning, đặc biệt trên những tác vụ khó. Đây chính là câu hỏi mà nghiên cứu tiếp theo bắt đầu đào sâu — điểm chuyển tự nhiên từ QLoRA → DoRA.\nDoRA Trong FFT, việc cập nhật tham số có mức độ tự do rất cao — model có thể thay đổi mạnh về hướng (direction) trong khi gần như giữ nguyên độ lớn (magnitude), hoặc ngược lại. Nhưng vì LoRA gộp cả hai chiều này vào chung một ma trận hạng thấp $\\Delta W \\approx BA$, nó vô tình trói buộc magnitude và direction lại với nhau, không phản ánh đúng hành vi học của FFT.\nNăm 2024, nhóm NVIDIA (Liu et al., ICML 2024 Oral, arXiv:2402.09353) đề xuất DoRA (Weight-Decomposed Low-Rank Adaptation), tách trọng số tiền huấn luyện $W$ thành một vector độ lớn $m$ và ma trận hướng $V / \\lVert V \\rVert_c$ độc lập:\n$$W = m \\cdot \\frac{V}{\\lVert V \\rVert_c}$$Trong đó $m$ (số tham số rất nhỏ) được update trực tiếp bằng gradient descent, còn $V$ (ma trận lớn) vẫn được cập nhật qua low-rank adaptation kiểu LoRA ($V + BA$). Nhờ tách riêng hai chiều tự do này, DoRA bắt chước sát hơn hành vi học của FFT.\nNhìn lại: khi nào dùng cái gì? Kỹ thuật Base model VRAM cần Chất lượng Khi nào dùng Full Fine-tuning Train toàn bộ Rất cao Cao nhất (baseline) Đủ GPU, cần hiệu năng tối đa, task khó LoRA Đóng băng, FP16/BF16 Trung bình Gần FFT ở hầu hết task Muốn train nhanh, giữ nhiều adapter cho nhiều task QLoRA Đóng băng, quantize 4-bit (NF4) Thấp Gần LoRA, đôi khi giảm nhẹ GPU hạn chế VRAM, cần fine-tune model lớn trên phần cứng nhỏ DoRA Đóng băng, tách magnitude/direction Tương đương LoRA Sát FFT hơn LoRA ở cùng số tham số Cần chất lượng cao hơn LoRA mà chưa đủ tài nguyên cho FFT Kết luận PEFT là mảng đang tiến rất nhanh. Chỉ tính riêng 2025–2026, hàng loạt variant đã công bố vượt DoRA trên cùng bộ benchmark commonsense reasoning: CORA, BaLoRA, ALLoRA, Echo-LoRA, và LoFT (paper 2026 báo cáo cải thiện tới +40% so với DoRA trên cùng thiết lập). Phần lớn các con số này là self-reported bởi chính nhóm đề xuất phương pháp, chưa có nhiều đánh giá độc lập rộng rãi — nên xem DoRA/QDoRA là một cột mốc quan trọng (đã được validate qua ICML 2024 Oral + triển khai thực tế trong Hugging Face PEFT), chứ không phải điểm dừng cuối cùng của PEFT.\n","permalink":"https://vo-hoang-kh4ng.github.io/posts/fine-tuning-llm/","summary":"Vì sao general model chưa đủ cho bài toán domain-specific, và fine-tuning giải quyết gì mà Prompt Engineering hay RAG không làm được. Đi từ format dữ liệu, sequence packing, Full Fine-tuning, đến LoRA, QLoRA và DoRA — cùng lý do từng kỹ thuật ra đời.","title":"Fine-tuning LLM: Từ Prompt, RAG đến LoRA, QLoRA và DoRA"},{"content":"Nguồn gốc \u0026amp; lịch sử Nếu nhìn lại quá trình phát triển của AI vài năm qua, bạn sẽ thấy một sự tiến hóa kiến trúc đáng kinh ngạc. Chỉ mới 2022–2024, tất cả chúng ta còn đang chật vật với nghệ thuật \u0026ldquo;múa phím\u0026rdquo; bằng prompt engineering, chúng ta đã tiến rất nhanh sang việc tối ưu ngữ cảnh (context engineering) và tích hợp công cụ (tool use). Không dừng lại ở đó, giới kỹ sư tiếp tục nhúng AI vào các vòng lặp tự trị (autonomous loops), xây dựng hệ thống trí nhớ dài hạn và thiết lập các tổ chức đa tác tử (multi-agent orchestration) phức tạp.\nDòng thời gian tiến hóa của các phương pháp kỹ thuật AI, từ Prompt Engineering (2022) đến Graph Engineering (2026).\nVào giữa năm 2026, Peter Steinberger (founder OpenClaw) đã hỏi một câu hỏi tưởng chừng vu vơ trên X nhưng trở nên viral: \u0026ldquo;Chúng ta vẫn đang nói về vòng lặp (loops) hay đã chuyển sang đồ thị (graphs) rồi?\u0026rdquo; Vài giờ sau, kỹ sư AI nổi tiếng Hamel Husain đưa ra một khẳng định xanh rờn: Nghe thì có vẻ giật gân, nhưng anh em khoan hãy đập bỏ code cũ. Thực ra khái niệm Loop Engineering mới nổi được tầm một tháng rưỡi là đã bộc lộ giới hạn. Khi giao cho agent mấy task phức tạp, nó rất dễ bị \u0026ldquo;ngáo\u0026rdquo;: kẹt trong những vòng lặp vô tận, tự nhiên bỏ qua các logic nghiệp vụ quan trọng, hoặc cứ gọi tool liên tục đến mức làm tràn luôn cả context window.\nCách diễn giải chính xác và hữu ích nhất không phải là \u0026ldquo;loop engineering is dead\u0026rdquo;, mà là: khi một vòng lặp đơn lẻ không còn đủ, thứ chúng ta thu được là một graph — và graph chứa các loop bên trong nó. Graph engineering là cái tên tháng 8/2026 này cho việc thiết kế agent như một hệ thống nhiều-thành-phần có trạng thái (stateful composite workflow): nhiều nhánh song song, bộ kiểm chứng, điểm bàn giao, trạng thái chia sẻ, và điều kiện dừng — thay vì nhồi mọi thứ vào trong một context chat duy nhất.\nTại sao lại cần Graph Engineering? Trong một hệ thống vòng lặp tác tử đơn lẻ (single-agent loop), mọi quyết định chuyển trạng thái đều bị ẩn bên trong ngữ cảnh của mô hình.\nTrong single-agent loop, LLM tự quyết định mọi thứ: khi nào tìm kiếm, khi nào dừng, kết quả đúng hay sai.\nĐiểm yếu chí mạng của single-agent loop nằm ở việc chúng ta trao cho LLM quá nhiều quyền kiểm soát. Mô hình tự quyết định khi nào cần tìm kiếm, khi nào đã gom đủ dữ liệu, và tự đánh giá xem kết quả đã đạt chuẩn hay chưa. Mô hình này chạy demo thì rất mượt, nhưng đưa lên production lại là một rủi ro quá lớn. LLM rất dễ bị mắc kẹt trong các vòng lặp vô tận, bỏ qua các logic nghiệp vụ cốt lõi, hoặc tự làm tràn context window của chính mình.\nThay vì để AI tự tung tự tác, Graph Engineering vạch ra các ranh giới thực thi rõ ràng. Nguyên tắc ở đây là: quy tắc kinh doanh đã biết thì phải dùng logic lập trình xác định để giải quyết. LLM chỉ được kích hoạt khi hệ thống cần diễn giải ngữ nghĩa, lập kế hoạch hoặc xử lý sự mơ hồ. Các AI Agent vẫn được giữ khả năng suy luận độc lập, nhưng chúng chỉ được phép hoạt động bên trong phạm vi của từng node (sẽ nói ở phần sau). Quyền điều hướng và kiểm soát toàn bộ hệ thống đã được tước bỏ và giao lại cho sơ đồ đồ thị.\nVậy Graph Engineering là gì? 💡 Về bản chất, Graph Engineering là nghệ thuật kiến trúc hóa một hệ thống tác tử, thông qua việc thiết kế tinh vi các điểm nút, hệ thống ranh giới phụ thuộc, cơ chế dịch chuyển trạng thái, các luồng thực thi, chốt kiểm định, lộ trình phục hồi và các ranh giới kiểm soát. Graph Engineering là nghệ thuật kiến trúc hóa một hệ thống Agent\nCác thành phần của Graph Engineering Một đồ thị điều khiển tác tử tiêu chuẩn bao gồm ba thành phần nguyên tử nền tảng:\n1. Node (Nút) Là một đơn vị thực thi độc lập. Một nút có thể là:\nMột lần gọi LLM Một lần con người xác nhận yêu cầu của Agent Một phép tính xác định (deterministic computation) … Đáng chú ý, không phải nút nào cũng là AI. Ví dụ, việc tính toán xem một hóa đơn có vượt quá ngưỡng phê duyệt hay không thì không cần dùng đến LLM. Nhưng việc hiểu xem một email có phải là yêu cầu hoàn tiền hay không thì có thể cần dùng đến LLM.\n2. Edges (Cạnh) Nếu các node đóng vai trò là những đơn vị xử lý chuyên biệt, thì các cạnh (edges) chính là hệ thống điều phối cốt lõi. Chúng đảm nhận nhiệm vụ xác định chính xác luồng thực thi tiếp theo, đại diện cho các quy tắc điều hướng nghiệp vụ của toàn bộ hệ thống.\nDirect edges — kết nối trực tiếp, không điều kiện, đầu ra của tác vụ trước lập tức trở thành đầu vào cho tác vụ sau mà không cần logic rẽ nhánh Conditional edges — rẽ nhánh dựa trên điều kiện, đầu ra của tác vụ trước sẽ được đưa vào các node nhánh khác nhau dựa trên các điều kiện logic được định nghĩa trước Parallel edges — rẽ nhánh song song, cho phép một tác vụ được gửi đến nhiều tác vụ con cùng lúc, giúp giảm latency và tổng hợp đa góc nhìn. Looping edges — Khi kết quả từ một node không đạt chuẩn (thông qua bộ kiểm định), cạnh này chuyển hướng luồng công việc quay ngược lại nút trước đó kèm theo ngữ cảnh lỗi, bắt buộc Agent phải tự sửa sai và tối ưu hóa trước khi được phép đi tiếp. Error edges — Xử lý lỗi, khi có lỗi xảy ra ở một node (ví dụ như model bị hallucination), edge này sẽ kích hoạt một nút xử lý lỗi chuyên dụng, giúp hệ thống có khả năng tự phục hồi. Human-controlled edges — chờ con người phê duyệt, edge này sẽ tạm dừng luồng công việc và chờ con người can thiệp hoặc phê duyệt khi thực thi các task có rủi ro. Event-triggered edges — kích hoạt bởi sự kiện bên ngoài, edge này sẽ kích hoạt luồng công việc dựa trên các sự kiện bên ngoài. Bảy loại cạnh cơ bản trong một đồ thị điều khiển tác tử.\n3. State (Trạng thái) Nếu các Nút (Nodes) là những nhân viên chuyên trách, các Cạnh (Edges) là luồng luân chuyển, thì State chính là \u0026ldquo;cuốn hồ sơ dự án\u0026rdquo; được truyền tay giữa họ. Nó lưu chuyển dọc theo các cạnh, giúp hệ thống đa tác tử không bị \u0026ldquo;mất trí nhớ\u0026rdquo; hay đứt đoạn ngữ cảnh khi giao việc từ bước này sang bước khác. Về mặt dữ liệu, thay vì nhồi nhét mọi thứ vào một khung chat duy nhất, State gom gói toàn bộ bối cảnh phiên chạy một cách có cấu trúc:\nNhiệm vụ gốc (Task): Yêu cầu đầu vào ban đầu. Tạo tác trung gian (Artifacts/Drafts): Các bản nháp, đoạn code, hoặc sản phẩm đang làm dở. Ghi chú nghiên cứu (Notes): Dữ liệu thô, kết quả tìm kiếm mà tác tử gom nhặt dọc đường. Chốt kiểm định (Verdicts/Checks): Trạng thái pass/fail, điểm số đánh giá từ các nút kiểm duyệt trước đó. 4. State Reducers (Cơ chế tổng hợp trạng thái) Trong các kiến trúc rẽ nhánh song song (fan-out), khi nhiều tác tử (ví dụ: ba tác tử nghiên cứu độc lập) cùng lúc trả về kết quả và cập nhật chung vào một trường dữ liệu, nguy cơ ghi đè (overwrite) hoặc thất thoát dữ liệu là cực kỳ lớn. Đây là lúc hệ thống cần đến Reducer (Bộ tổng hợp/giảm). Reducer đóng vai trò như một \u0026ldquo;trọng tài\u0026rdquo; tại điểm hội tụ (fan-in), tự động áp dụng các quy tắc hợp nhất dữ liệu — như nối danh sách, gộp từ điển, hoặc giải quyết xung đột — để đảm bảo trạng thái hệ thống được cập nhật an toàn và toàn vẹn.\n5. Checkpoints (Điểm lưu trạng thái) Checkpoint là cơ chế snapshot của toàn bộ đồ thị tại một thời điểm thực thi cụ thể. Trong production, Checkpoint không chỉ để chống mất mát dữ liệu mà còn là nền tảng cốt lõi để workflow có thể:\nPhục hồi sau lỗi: Khôi phục tức thì sau các sự cố crash, lỗi mạng, hoặc bảo vệ thành quả của các tác vụ chạy nền dài hạn khỏi nguy cơ bị timeout. Gỡ lỗi \u0026amp; Kiểm toán: Cho phép kỹ sư truy xuất ngược về các trạng thái trong quá khứ để theo dõi chính xác từng quyết định của AI. Phát lại: Tua lại và chạy tiếp quy trình từ đúng một điểm bị lỗi thay vì phải gọi lại (tốn tiền token) từ đầu. 6. Interrupts (Ngắt) Nếu Checkpoint là cơ chế lưu trữ, thì Interrupt là \u0026ldquo;phanh khẩn cấp\u0026rdquo; của hệ thống. Nó tạm đóng băng luồng thực thi để nhường quyền kiểm soát cho các yếu tố bên ngoài (human-in-the-loop). Kỹ thuật này bắt buộc phải có khi: Chờ phê duyệt (Approval): Trước khi AI thực hiện các hành động có rủi ro cao (gửi email cho tệp khách hàng lớn, thực thi lệnh xóa database, v.v.). Xử lý sự mơ hồ: Yêu cầu con người cung cấp thêm dữ liệu đầu vào khi tác tử không thể (hoặc không được phép) tự suy luận tiếp.\nCách hoạt động trong LangGraph: Khi một interrupt xảy ra, LangGraph sẽ tự động lưu checkpoint của đồ thị. Sau đó, nó cho phép quy trình thực thi được tiếp tục (resume) vào một thời điểm sau đó bằng cách sử dụng cùng một định danh luồng (thread identifier).\n💡 Mẹo kỹ thuật: Tài liệu chính thức khuyến nghị rằng bạn nên đảm bảo các tác dụng phụ (side effects) xảy ra trước điểm ngắt phải có tính lũy đẳng (idempotent).\nGiải thích nhanh: \u0026ldquo;Lũy đẳng\u0026rdquo; nghĩa là dù thao tác đó có bị thực thi lại nhiều lần do cơ chế retry hoặc resume, nó cũng chỉ tạo ra cùng một kết quả cuối cùng, không gây ra lỗi trùng lặp dữ liệu hay hành động thừa — ví dụ: không gửi 2 email giống hệt nhau cho cùng một người.\nGraph Patterns Pattern 1: Prompt Chaining Prompt Chaining đóng vai trò là một khối xây dựng cơ bản (building block) cho các đồ thị tác tử, đặc biệt khi hệ thống được mở rộng thành các luồng công việc phi tuyến tính hoặc có tính chu kỳ. Về mặt cơ chế, đầu ra của nút này sẽ trở thành đầu vào cho nút tiếp theo.\nPrompt Chaining với context isolation: mỗi nút bắt đầu với \u0026ldquo;cửa sổ ngữ cảnh sạch\u0026rdquo;, chỉ chứa dữ liệu cần thiết.\nNhiều tài liệu thường giải thích pattern này theo hướng chuyên môn hóa vai trò (ví dụ: tác tử nghiên cứu → tác tử viết bài → biên tập viên). Tuy nhiên, cách giải thích đó đã bỏ qua một lợi ích mang tính kiến trúc cốt lõi nhất của việc chia chuỗi: Cô lập ngữ cảnh (Context Isolation).\nTại sao cần cô lập ngữ cảnh? Trong bước đầu tiên của luồng công việc, tác tử thường tích lũy một lượng lớn token chứa các dữ liệu thô, kết quả tìm kiếm, tool output, hoặc cả những suy luận sai lầm. Bước thứ hai thường không cần đến đống dữ liệu hỗn độn này, và việc nhồi nhét chúng vào sẽ làm suy giảm chất lượng kết quả một cách rõ rệt. Việc thiết lập ranh giới (node boundary) trong đồ thị giúp tác tử ở bước hai bắt đầu với một \u0026ldquo;cửa sổ ngữ cảnh sạch\u0026rdquo;, chỉ chứa đúng những thông tin mà nó cần.\nPattern này hoạt động tốt nhất khi một tác vụ có thể được phân rã rõ ràng thành các bài toán nhỏ (subtasks), trong đó mỗi bước có yêu cầu về ngữ cảnh hoàn toàn khác biệt. Ví dụ: Nút trích xuất tạo ra dữ liệu có cấu trúc; Nút định dạng chỉ cần dữ liệu đó chứ không cần toàn bộ văn bản thô ban đầu.\nDấu hiệu nên bỏ qua (Skip when): Khi các bước quá gắn kết với nhau khiến bạn đằng nào cũng phải chuyển tiếp toàn bộ trạng thái (state) đi tiếp. Dấu hiệu nhận biết rõ nhất là khi đối tượng trạng thái của hệ thống chỉ liên tục \u0026ldquo;phình to\u0026rdquo; qua mỗi bước, và mọi nút đều phải đọc toàn bộ kết quả của nút trước đó. Lúc này, bạn chỉ đang có \u0026ldquo;một nút với 3 prompt khác nhau\u0026rdquo;, và việc thêm các bước chuyển nút (hops) chỉ làm tăng độ trễ (latency) một cách vô ích.\n3 nguyên tắc triển khai an toàn Để triển khai Prompt Chaining an toàn và hiệu quả, các kỹ sư cần lưu ý 3 nguyên tắc sau tại ranh giới giữa các nút:\nThiết lập danh sách trắng (Whitelist): Lựa chọn và chỉ định rõ ràng những dữ liệu nào mỗi nút được phép nhận, thay vì truyền toàn bộ trạng thái chia sẻ (shared state) một cách bừa bãi. Định nghĩa kiểu dữ liệu bàn giao (Type the handoff): Giao tiếp giữa các nút cần có lược đồ dữ liệu (schema) cụ thể. Nếu có sự thay đổi về cấu trúc, hệ thống sẽ vỡ (break) ngay tại ranh giới đó để dễ gỡ lỗi, thay vì lan truyền lỗi xuống tận 3 nút hạ nguồn. Tóm tắt tại biên (Summarize at the edge): Nếu đầu ra của nút thượng nguồn (upstream) quá lớn, hãy tóm tắt nó. Bản tóm tắt thường cung cấp chính xác những gì nút tiếp theo cần và có chi phí prompt rẻ hơn hẳn một bậc. Pattern 2: Routing Trong Kỹ thuật Đồ thị, định tuyến (routing) là một mẫu thiết kế quen thuộc nhưng lại được xem là \u0026ldquo;bước tối ưu cấu trúc mang lại hiệu quả cao nhất với chi phí thấp nhất\u0026rdquo; (the cheapest structural win). Ở pattern này, một nút làm nhiệm vụ phân loại dữ liệu đầu vào, sau đó một \u0026ldquo;cạnh có điều kiện\u0026rdquo; (conditional edge) sẽ điều hướng luồng công việc xuống một trong nhiều nhánh xử lý chuyên biệt.\nRouting pattern: khớp chi phí với độ khó — tác vụ đơn giản đi nhánh rẻ, tác vụ phức tạp đi nhánh đắt.\nTrong thực tế, bạn có thể thiết kế hai loại bộ định tuyến:\nĐịnh tuyến xác định (Deterministic routing): Sử dụng các quy tắc lập trình (code if/else) nếu các tiêu chí phân loại đã rõ ràng và chính xác. Định tuyến dựa trên mô hình (Model-based routing): Gọi một LLM để phân loại khi đầu vào mang tính mơ hồ và cần diễn giải ngữ nghĩa. Đáng chú ý, nút định tuyến không trực tiếp điều khiển đồ thị; nó chỉ cập nhật hoặc đọc trạng thái (state) để quyết định cạnh nào được phép kích hoạt. Tại sao Routing lại quan trọng? Lợi ích cốt lõi của pattern này là cho phép bạn khớp chi phí với độ khó của bài toán (match cost to difficulty).\nVí dụ thực tế:\nXử lý Ticket Hỗ trợ (Support Graph): Một đồ thị phân loại ticket có thể gửi các yêu cầu đơn giản (đổi mật khẩu, hỏi giờ làm việc) đến một mô hình nhỏ để tiết kiệm token và tăng tốc độ phản hồi. Ngược lại, các vấn đề mơ hồ và phức tạp sẽ được định tuyến tới mô hình lớn (như GPT-5 hay Claude Sonnet 5) với khả năng suy luận mạnh mẽ hơn. Đánh giá Mã nguồn (Code Review): Khi đánh giá một Pull Request (PR), hệ thống có thể định tuyến dựa trên \u0026ldquo;bán kính ảnh hưởng\u0026rdquo; (blast radius). Các lỗi typo hoặc cập nhật document đi vào nhánh giá rẻ; các thay đổi liên quan đến module xác thực (auth), thanh toán (payments) hay di chuyển dữ liệu (migrations) sẽ đi vào nhánh đắt tiền hơn — sử dụng nhiều công cụ kiểm chứng hơn và bắt buộc có cổng chờ con người phê duyệt (human approval) trước khi merge. Tiêu chí áp dụng Điểm lý tưởng (Best used): Routing phát huy tối đa sức mạnh khi các nhánh thực thi có sự khác biệt rõ rệt về mặt bản chất: sử dụng các mô hình khác nhau, bộ công cụ khác nhau, ngân sách khác nhau hoặc yêu cầu phê duyệt khác nhau. Dấu hiệu nên bỏ qua (Skip when): Nếu hai nhánh của bạn thực chất chỉ chạy cùng một nút duy nhất với một câu lệnh (prompt) được tinh chỉnh hơi khác một chút, thì đó là một \u0026ldquo;tham số\u0026rdquo; (parameter), không phải là một \u0026ldquo;nhánh đồ thị\u0026rdquo; (path). Đừng vẽ thêm nhánh nếu không có sự khác biệt về luồng công việc. Cạm bẫy \u0026ldquo;Cái chết âm thầm\u0026rdquo; (Silent Failure) Điểm yếu chí mạng của pattern này là khi bộ định tuyến trở thành mắt xích yếu nhất trong hệ thống.\nNếu mọi bước xử lý ở hạ nguồn (downstream) đều hoàn toàn phụ thuộc vào kết quả phân loại của bộ định tuyến, và bộ định tuyến lại dự đoán sai, hệ thống sẽ không báo lỗi (doesn\u0026rsquo;t fail loudly). Thay vào đó, tác vụ bị định tuyến sai vẫn sẽ hoàn thành một cách trơn tru, nhưng đi nhầm đường và tạo ra một kết quả hoàn toàn vô nghĩa.\nBest Practices để xây dựng Router Đo lường bằng dữ liệu thực: Hãy kiểm định và đánh giá (validate) bộ định tuyến bằng các tập dữ liệu mẫu đã gán nhãn trước khi đưa lên production, và liên tục giám sát độ chính xác của nó. Phần lớn sự cố của mô hình định tuyến đến từ việc chúng phân loại sai. Thiết lập đường lùi \u0026ldquo;Bảo thủ\u0026rdquo; (Conservative Fallback): Đối với các đầu vào mà hệ thống không nhận diện được hoặc có độ tự tin thấp, hãy thiết lập cho chúng cùng một đích đến: Nhánh bảo thủ nhất. Thông thường, đây là nhánh đắt tiền nhất hoặc nhánh yêu cầu con người can thiệp. Việc đẩy một công việc bị định tuyến sai hoặc không rõ ràng vào một \u0026ldquo;nhánh giá rẻ mặc định\u0026rdquo; chính là công thức hoàn hảo để tạo ra rác. Pattern 3: Parallelization Trong kiến trúc đồ thị, xử lý song song (parallelization) là cơ chế rẽ nhánh đồng thời (fan-out) luồng công việc tới nhiều nút xử lý cùng lúc và sau đó hội tụ lại tại một điểm nối (fan-in). Việc thực thi đồng thời các luồng công việc độc lập chính là một trong những lợi ích cốt lõi nhất của hệ thống đồ thị, điều mà cấu trúc vòng lặp đơn lẻ (single loop) về mặt cấu trúc không thể thực hiện được.\nFan-out/Fan-in: nhiều worker chạy đồng thời, kết quả hội tụ tại nút join thông qua state reducer.\nLợi ích chiến lược: Giảm độ trễ và Kiểm chứng đa chiều Mẫu thiết kế này đặc biệt tỏa sáng trong các trường hợp yêu cầu độ trễ thấp hoặc đòi hỏi sự tổng hợp từ nhiều góc nhìn khác nhau:\nTối ưu hóa độ trễ cho ứng dụng thời gian thực: Chẳng hạn, một tổng đài viên AI (AI phone agent) hoạt động trên các cuộc gọi thoại có ngân sách độ trễ chỉ tính bằng mili-giây. Hệ thống phải xử lý giọng nói đầu vào, gửi đến một tác tử nhận diện ý định, sau đó rẽ nhánh song song tới các tác tử khác để tra cứu tài khoản, kiểm tra chính sách và chi tiết hợp đồng cùng lúc. Cuối cùng, ngữ cảnh sẽ hội tụ lại tại một nút soạn thảo câu trả lời và đi qua cổng kiểm duyệt trước khi phản hồi. Đa góc nhìn và đánh giá chéo: Khi cần các kết quả có độ tin cậy cao, việc triển khai nhiều tác tử hoạt động song song là vô cùng hữu ích. Chẳng hạn, trong một sản phẩm bảo mật, bạn có thể triển khai hai hay nhiều tác tử độc lập cùng đồng thời rà soát một đoạn mã nguồn để tìm kiếm lỗ hổng bảo mật. Những cạm bẫy kiến trúc cần lưu ý Việc lạm dụng xử lý song song có thể dẫn đến những hệ quả tiêu cực về cả chi phí lẫn hiệu năng:\nĐánh đổi tài chính lấy độ trễ (Buying latency with money): Nếu các luồng công nhân (workers) chạy song song yêu cầu đọc cùng một ngữ cảnh đầu vào, bạn đang phải trả chi phí xử lý token gấp nhiều lần cho cùng một lượng ngữ cảnh đó. Đây là sự đánh đổi xứng đáng cho user-facing tasks, nhưng lại lãng phí cho batch jobs. Nút thắt cổ chai tại nhánh chậm nhất: Tốc độ cải thiện của toàn bộ hệ thống bị giới hạn bởi độ trễ của nhánh thực thi chậm nhất. Nếu bạn có bốn nút hoàn thành trong 10 giây nhưng một nút mất tới 90 giây, thời gian chờ tại điểm hội tụ (join) vẫn sẽ là hơn 90 giây. Thảm họa tranh chấp dữ liệu (Race Conditions): Khi rẽ nhánh song song, việc hai hay nhiều nút cùng thực hiện thay đổi trên một trường dữ liệu của trạng thái chia sẻ (shared state) sẽ dẫn đến tình trạng tranh chấp không xác định. Best Practices cho môi trường sản xuất Dữ liệu bất biến tại các nhánh (Immutable Branching): Tuyệt đối không cho phép các nhánh song song trực tiếp thay đổi (mutate) trạng thái chung. Các nhánh chỉ nên trả về kết quả độc lập dưới dạng dữ liệu bất biến. Việc gộp dữ liệu phải được thực hiện một cách tường minh tại nút hội tụ (join node) thông qua các bộ giảm trạng thái (state reducers), với quy tắc thiết kế chuẩn mực là mỗi trường dữ liệu chỉ được phép có duy nhất một nút có quyền ghi. Thiết lập chính sách chịu lỗi một phần (Partial Results \u0026amp; Timeouts): Hệ thống không thể phó mặc dòng thực thi khi một nhánh gặp sự cố. Bạn cần thiết lập giới hạn thời gian (timeouts) riêng cho từng nhánh và quyết định rõ ràng chính sách tại nút hội tụ. Ví dụ: nếu chỉ có ba trên năm nguồn dữ liệu trả về kết quả, hệ thống sẽ tiếp tục thực thi với kết quả một phần, tiến hành thử lại (retry), hay lập tức đánh rớt toàn bộ phiên chạy? Pattern 4: Orchestrator-Workers Trong Kỹ thuật Đồ thị, mô hình Điều phối viên - Công nhân (Orchestrator-Workers) là một bước tiến xa hơn so với xử lý song song thông thường. Ở pattern này, một nút đóng vai trò là \u0026ldquo;người điều phối\u0026rdquo; sẽ tự động phân rã một nhiệm vụ tổng thể, quyết định ngay tại thời gian chạy (runtime) xem cần sinh ra (spawn) bao nhiêu tác tử công nhân, mỗi công nhân sẽ làm gì, và cuối cùng tổng hợp kết quả của chúng.\nOrchestrator-Workers: hình thù đồ thị chỉ được xác định tại thời gian chạy.\nĐiểm đặc biệt nhất của pattern này là bạn hoàn toàn không biết trước độ rộng rẽ nhánh (fan-out width), danh tính của các công nhân hay hình thù của đồ thị cho đến khi nó thực sự chạy.\nTại sao và khi nào nên sử dụng? Điểm lý tưởng (Best used): Mẫu kiến trúc này tỏa sáng khi danh sách các công việc con không thể được liệt kê trước khi bắt đầu do phạm vi nhiệm vụ quá rộng. Ví dụ: Cấu trúc lại một API cũ trên một kho mã nguồn lớn. Bạn sẽ không thể định nghĩa một đồ thị tĩnh vì bạn chưa biết API đó được dùng ở những tệp nào. Thay vào đó, tác tử điều phối sẽ quét kho mã nguồn để khám phá danh sách công việc, sau đó tự động rẽ nhánh tạo ra một tác tử công nhân cho mỗi tệp hoặc mỗi module bị ảnh hưởng. Dấu hiệu nên bỏ qua (Skip when): Nếu bạn có thể tự viết ra danh sách tác vụ hoặc trích xuất nó bằng một câu truy vấn (query) đơn giản, hãy quay về sử dụng Pattern 3 (Parallelization) để giữ cho cấu trúc đồ thị là tĩnh và dễ kiểm soát. Những \u0026ldquo;bất ngờ\u0026rdquo; trên môi trường sản xuất Sự bùng nổ rẽ nhánh (Unbounded fan-out): Mặc định, độ rộng rẽ nhánh của điều phối viên là không có giới hạn. Nếu nó quyết định sinh ra 40 công nhân và cả 40 cùng gọi đến một API bị giới hạn băng thông, hệ thống sẽ gặp lỗi quá tải (429 Too Many Requests) và sụp đổ. Cấu trúc Topo không xác định (Non-deterministic topology): Hai lần chạy trên cùng một đầu vào có thể tạo ra hai hình thù đồ thị khác nhau, phá vỡ phương pháp gỡ lỗi truyền thống. Thảm họa tổng hợp dữ liệu (Aggregation complexity): Các kết quả có thể trả về không theo thứ tự, một số công nhân có thể gặp lỗi giữa chừng, hoặc công nhân trả lời một vấn đề hơi chệch hướng so với kế hoạch ban đầu. Một nút tổng hợp không có cơ chế đối chiếu kết quả với kế hoạch gốc sẽ tạo ra một bản báo cáo đầy tự tin nhưng lại thiếu mất 20% lượng công việc. Best Practices Thiết lập trần giới hạn (Explicit Ceilings): Phải đặt ra các giới hạn nghiêm ngặt về độ rộng rẽ nhánh, độ sâu đệ quy, và chi phí tài chính (spend ceilings). Quản lý ngân sách theo nhánh (Subtree Token Budget): Hãy thiết lập ngân sách token cho toàn bộ một nhánh phân cấp (subtree) thay vì cấp ngân sách cho từng công nhân lẻ tẻ. Lưu trữ bản kế hoạch như một tạo tác bất biến (Durable Artifact): Hãy luôn lưu lại bản kế hoạch phân công ban đầu. Khi một phiên chạy bị lỗi, câu hỏi thường trực luôn là \u0026ldquo;Kế hoạch phân rã bị sai, hay quá trình thực thi của công nhân bị hỏng?\u0026rdquo;. Bạn sẽ không thể trả lời câu hỏi này nếu không có bản kế hoạch gốc để đối chiếu. Pattern 5: Evaluator-Optimizer Về bản chất, mẫu \u0026ldquo;Người đánh giá - Người tối ưu hóa\u0026rdquo; chính là một vòng lặp (loop) được đặt bên trong một đồ thị. Trong pattern này, quy trình hoạt động theo một chu trình khép kín: một thành phần tạo ra kết quả (generate), một thành phần khác đánh giá (evaluate), vòng lặp sẽ quay lại nếu thất bại và thoát ra khi thành công hoặc khi chạm một ngưỡng giới hạn đã thiết lập.\nEvaluator-Optimizer: vòng lặp có kiểm soát — generate, evaluate, lặp lại cho đến khi đạt hoặc hết quota.\nTại sao và khi nào nên sử dụng? Mô hình này hoạt động hiệu quả nhất khi các tiêu chí đánh giá được xác định rõ ràng và mỗi lần sửa đổi đều mang lại sự cải thiện có thể đo lường được.\nĐiểm lý tưởng (Best used): Khi bạn có thể định nghĩa điều kiện thành công (pass condition) ngay từ trước khi chạy và có thể kiểm tra nó một cách tự động. Ví dụ: Một đồ thị có nhiệm vụ tối ưu hóa một truy vấn cơ sở dữ liệu bị chậm — bộ tạo (generator) viết lại truy vấn, bộ đánh giá (evaluator) chạy thử trên tập dữ liệu đại diện để kiểm tra thời gian thực thi. Điều kiện đạt: phản hồi dưới 200ms.\nCạm bẫy \u0026ldquo;Vòng lặp Vô nghĩa\u0026rdquo; Pattern này dễ thất bại và gây lãng phí nhất khi các điều kiện thoát (exit conditions) không được định nghĩa chi tiết. Ngoài \u0026ldquo;thành công\u0026rdquo; và \u0026ldquo;chạm giới hạn số vòng lặp tối đa\u0026rdquo;, có 4 trạng thái thất bại \u0026ldquo;ẩn\u0026rdquo; mà kỹ sư thường bỏ quên:\nKhông có tiến triển (No progress): Hệ thống trả về cùng một mã lỗi hoặc cùng một bản nháp lặp đi lặp lại — vòng lặp chỉ đang lấy mẫu lại (resampling) một cách vô ích. Dao động (Oscillation): Hệ thống nhảy từ trạng thái A, sang B, rồi quay lại A — bản sửa lỗi này lại làm hỏng thành phần khác. Cạn kiệt ngân sách Token: Các lần thử sau sẽ đắt đỏ hơn lần thử trước do ngữ cảnh (context) bị tích lũy dần; 10 vòng lặp không có nghĩa là chi phí chỉ gấp 10 lần vòng lặp đầu tiên. Chi phí tăng nhưng điểm đánh giá không đổi: Tín hiệu rõ ràng nhất cho thấy bạn đang \u0026ldquo;đốt tiền\u0026rdquo; vào bài toán mà generator không đủ khả năng giải quyết. Best Practices Ghi log chi tiết từng vòng lặp: Hãy ghi lại phán quyết và lý do của bộ đánh giá ở mọi vòng lặp. Một vòng lặp phải thất bại 9 lần rồi mới qua sẽ cho bạn nhiều thông tin để tinh chỉnh hệ thống hơn là một vòng lặp thành công ngay lần đầu. Cô lập ngữ cảnh lỗi: Trừ khi bộ tạo thực sự cần đọc lại những nỗ lực đã thất bại để rút kinh nghiệm, hãy giữ các bản nháp hỏng ra khỏi cửa sổ ngữ cảnh của lần thử tiếp theo. Thông thường, chỉ cần truyền lại thông báo lỗi là đủ. Phân định rõ lý do dừng: Khi vòng lặp thoát vì chạm giới hạn số lần thử chứ không phải vì thành công, hãy xử lý nó như một trạng thái hoàn toàn riêng biệt. Các nút hạ nguồn cần phản ứng khác nhau đối với \u0026ldquo;chúng ta đã ngừng thử\u0026rdquo; so với \u0026ldquo;cách này hoàn toàn không hoạt động\u0026rdquo;. Xu hướng mở rộng GraphRAG: Xử Lý Ngữ Cảnh Toàn Cục Retrieval-Augmented Generation (RAG) truyền thống dựa trên tìm kiếm vector cực kỳ hiệu quả với các truy vấn cục bộ. Tuy nhiên, nó thất bại thảm hại trước các câu hỏi yêu cầu hiểu biết toàn cục (global sensemaking) trên toàn bộ tập dữ liệu.\nGraphRAG giải quyết vấn đề này bằng cách sử dụng LLM để trích xuất các thực thể và mối quan hệ, từ đó xây dựng một đồ thị tri thức phân cấp (hierarchical knowledge graph). Hai kỹ thuật tối ưu hóa đang đưa GraphRAG lên tầm cao mới:\nDynamic Community Selection: Dùng một LLM nhỏ (như GPT-5 mini) để chấm điểm và cắt tỉa (prune) các nhánh không liên quan ngay từ sớm — giảm trung bình 77% chi phí token mà vẫn giữ nguyên chất lượng câu trả lời. GraphRAG-FI (Filtering \u0026amp; Integration): Áp dụng bộ lọc hai giai đoạn và tích hợp dựa trên phân phối xác suất (logits) để cân bằng giữa tri thức bên ngoài và khả năng tự suy luận của mô hình. Graph Transformers: Cách Mạng Hóa Khả Năng Hiểu Đồ Thị Việc đưa dữ liệu đồ thị vào các mô hình Transformer tiêu chuẩn từng là một bài toán khó. Các kiến trúc Graph Transformers (GTs) đã vượt qua giới hạn của mạng nơ-ron đồ thị (GNN) truyền thống:\nGraphGPT: Sử dụng kỹ thuật tuần tự hóa dựa trên các chu trình Euler để chuyển đổi đồ thị thành chuỗi token không mất mát thông tin. Mở rộng lên tới 2 tỷ tham số và đạt hiệu năng SOTA trên các tập dữ liệu OGB lớn. Relational Graph Transformer (RelGT): Phân rã mỗi nút thành 5 thành phần token: đặc trưng, loại, khoảng cách bước duyệt, thời gian và cấu trúc cục bộ. GraphTARIF: Tích hợp mô-đun mạng cục bộ để tăng hạng (rank), đồng thời dùng hàm lũy thừa lôgarit có khả năng học để làm sắc nét sự phân bổ trọng số chú ý. Hạ Tầng: Chuẩn GQL và Graph Databases Tháng 4/2024 đánh dấu bước ngoặt khi ISO GQL (Graph Query Language) chính thức trở thành ngôn ngữ truy vấn cơ sở dữ liệu độc lập mới đầu tiên được ISO phê chuẩn kể từ SQL năm 1987.\nKhi xây dựng hạ tầng, các kỹ sư cần cân nhắc sự khác biệt về kiến trúc lưu trữ:\nDatabase Kiến trúc Điểm mạnh Memgraph In-Memory First (C++) Độ trễ cực thấp, phân tích luồng thời gian thực FalkorDB Tích hợp trên Key-Value Khởi động nhanh, hiệu năng có thể đạt 496x TigerGraph Native Parallel Graph Sharding tự động, batch write siêu tốc, hàng chục tỷ đỉnh Tổng Kết Graph Engineering không chỉ là một thuật ngữ \u0026ldquo;bắt trend\u0026rdquo; mới. Nó là sự công nhận rằng việc xây dựng các hệ thống AI cấp doanh nghiệp đòi hỏi sự kiểm soát nghiêm ngặt về mặt kiến trúc. Bằng cách cấu trúc hóa luồng công việc thông qua đồ thị (Agent Graph), tăng cường khả năng truy xuất bằng GraphRAG, vận dụng các Graph Transformers hiện đại và dựa trên hạ tầng GQL vững chắc, chúng ta đang bước vào một kỷ nguyên mới của Trí tuệ Nhân tạo — nơi AI không chỉ \u0026ldquo;suy nghĩ\u0026rdquo; mà còn được kiến trúc hóa để hoạt động an toàn, có kiểm soát và có thể mở rộng trong môi trường thực tế.\nBài viết tổng hợp và phân tích về xu hướng Graph Engineering trong thiết kế hệ thống AI Agent, dựa trên các thảo luận và tài liệu kỹ thuật từ cộng đồng AI giữa năm 2026.\n","permalink":"https://vo-hoang-kh4ng.github.io/posts/graph-engineering/","summary":"Loop Engineering Is Dead. Enter Graph Engineering — khi một vòng lặp đơn lẻ không còn đủ, thứ bạn thu được là một đồ thị. Tổng hợp và phân tích về xu hướng kiến trúc đồ thị cho hệ thống AI agent đa thành phần.","title":"Graph Engineering: Kiến trúc đồ thị cho hệ thống AI Agent thế hệ mới"},{"content":"Mình vừa đọc xong một báo cáo khá thú vị của Google DeepMind, tựa đề \u0026ldquo;Conjecture Machines: AI agents and the new validation bottleneck in science\u0026rdquo; (tháng 7/2026), do Don Wallace, Conor Griffin, Sean O\u0026rsquo;Neill, Thang Luong và Owen Larter viết, dựa trên phỏng vấn 10 nhà nghiên cứu/kỹ sư của DeepMind. Nội dung xoay quanh một câu hỏi lớn: khi AI agent bắt đầu tự đề xuất giả thuyết, thiết kế thí nghiệm, thậm chí khám phá ra thuật toán tốt hơn con người — thì đâu là điểm nghẽn thật sự của khoa học?\nCâu trả lời của báo cáo, gói gọn trong đúng một câu: tạo ý tưởng đang trở nên rẻ, nhưng kiểm chứng ý tưởng thì không.\nCâu chuyện mở đầu: khi agent \u0026ldquo;đoán\u0026rdquo; đúng thứ mất cả thập kỷ để chứng minh Báo cáo mở đầu bằng câu chuyện của nhà vi sinh vật học José Penadés (Imperial College London). Nhóm của ông mất gần một thập kỷ để tìm ra cách một họ siêu vi khuẩn kháng kháng sinh lây lan — kết quả chưa từng công bố, chỉ có nội bộ phòng lab biết.\nNăm 2024, ông thử mô tả vấn đề này cho Co-Scientist, một AI agent của Google DeepMind. Trong vòng hai ngày, agent trả về 5 giả thuyết, xếp hạng ưu tiên. Giả thuyết số 1 chính là điều nhóm ông đã mất nhiều năm để chứng minh: một số siêu vi khuẩn \u0026ldquo;mượn\u0026rdquo; đuôi virus làm chìa khóa để nhảy giữa các loài vật chủ. Ông sốc đến mức nghĩ máy tính bị rò rỉ dữ liệu, phải email Google để xác minh — không có gì bị lộ cả.\nĐây không phải câu chuyện \u0026ldquo;AI thay thế nhà khoa học\u0026rdquo;, mà là minh hoạ cho điều báo cáo muốn nói: agent có thể nén một quá trình tư duy nhiều năm xuống còn vài ngày.\nVì sao agent bỗng dưng hữu ích đến vậy? Báo cáo chỉ ra ba lực đẩy, xếp thành một kim tự tháp:\nFrontier model mạnh hơn — các model hàng đầu giờ vượt qua chuyên gia con người trên những benchmark khó như Humanity\u0026rsquo;s Last Exam hay FrontierMath, nhờ inference-time reasoning: model \u0026ldquo;suy nghĩ\u0026rdquo; qua nhiều bước, tự khám phá và tự sửa trước khi chốt câu trả lời. Scaffolding — lớp \u0026ldquo;khung\u0026rdquo; bọc quanh model, cho nó bộ nhớ, khả năng gọi công cụ (chạy code, tra cứu literature, gọi các model chuyên biệt khác). Scaffolding đời đầu thường phải làm thủ công cho từng agent, khó tái sử dụng; các giao thức giao tiếp agent-với-agent mới đang giảm bớt gánh nặng này. Customisation — agent \u0026ldquo;skill\u0026rdquo;: những tập lệnh dạng văn bản đơn giản, cho phép nhà khoa học \u0026ldquo;dạy\u0026rdquo; agent kiến thức ngầm (tacit knowledge) của riêng họ — thứ vốn khó truyền đạt bằng lời, chỉ tích luỹ qua năm tháng làm nghề. Ba lớp làm nên một agent hữu ích, theo báo cáo: model nền càng mạnh, scaffolding càng tốt thì càng \u0026ldquo;khai thác\u0026rdquo; được nhiều năng lực tiềm ẩn, và customisation là lớp giúp agent hiểu tri thức ngầm riêng của từng nhà khoa học.\nCó một câu nói của nhà khoa học tính toán Natasha Latysheva mình rất thích: \u0026ldquo;Nghiên cứu khoa học sẽ chuyển từ thực thi trực tiếp sang điều phối ở tầm cao. Chúng ta sẽ bắt đầu ngày làm việc bằng cách xem lại những thí nghiệm agent chạy qua đêm.\u0026rdquo;\nAgent giỏi \u0026ldquo;nghĩ ra ý tưởng\u0026rdquo;, nhưng không giỏi \u0026ldquo;chứng minh nó đúng\u0026rdquo; Đây là phần mình thấy đáng suy ngẫm nhất của báo cáo. Có ba giai đoạn trong một chu trình khoa học: ideation (nghĩ giả thuyết) → tìm nghiệm tối ưu → validation (kiểm chứng). Agent đang tạo ra bước nhảy vọt ở hai giai đoạn đầu, nhưng gần như chưa giúp gì nhiều ở giai đoạn cuối.\nBa giai đoạn của một chu trình khoa học. Agent đang tạo bước nhảy vọt rõ rệt ở \u0026ldquo;ideation\u0026rdquo; và \u0026ldquo;tìm nghiệm tối ưu\u0026rdquo; — nhưng \u0026ldquo;validation\u0026rdquo; thì gần như vẫn giậm chân tại chỗ, vì phải chạm vào thực tế vật lý.\nIdeation: Co-Scientist điều phối một dàn subagent để sinh ra nhiều giả thuyết, tự phản biện, xếp hạng, lặp lại — mô phỏng cách một nhóm nghiên cứu con người vận hành, nhưng ở tốc độ khác hẳn. Ví dụ thực tế: Gary Peltz (Stanford) tìm thuốc có thể tái sử dụng để trị xơ gan. Ông chọn 2 ứng viên dựa trên kinh nghiệm nhiều thập kỷ — không cái nào hiệu quả. Co-Scientist chọn 3 ứng viên — 2 trong số đó không chỉ chặn được xơ hoá mà còn thúc đẩy tái tạo tế bào gan.\nNhưng đi kèm là rủi ro rất thật: \u0026ldquo;Một tuyên bố ảo giác (hallucination) duy nhất ở trang 10 của kết quả có thể làm mất giá trị toàn bộ tài liệu\u0026rdquo; — lời của Vivek Natarajan, trưởh nhóm Co-Scientist. Ông gọi thứ agent còn thiếu là \u0026ldquo;epistemic humility\u0026rdquo; — sự khiêm tốn nhận thức: biết khi nào mình không biết, và nói ra điều đó. AlphaFold làm được điều này (báo cáo độ tin cậy cho từng phần dự đoán), nhưng để hiệu chỉnh độ tự tin cho các suy luận khoa học mở hơn thì vẫn là bài toán chưa có lời giải.\nTìm nghiệm tối ưu: AlphaEvolve là ví dụ agent tìm kiếm nghiệm tốt nhất chứ không chỉ nghiệm chạy được, trong không gian giải pháp gần như vô hạn (thiết kế vật liệu, thuật toán\u0026hellip;). Nó đã góp phần thiết kế chip TPU thế hệ mới của Google, giúp nhà toán học Terence Tao giải các bài toán Erdős còn mở, và cải thiện phân tích dữ liệu gen. Nhưng ngay cả với AlphaEvolve, kết quả đầu ra trong các lĩnh vực như vật liệu vẫn chỉ là ứng viên tiềm năng, chưa phải kết quả cuối — chất xúc tác vẫn phải được tổng hợp và đo đạc thật ở phòng lab.\nValidation — nút thắt thật sự: Báo cáo dùng lại ý của Karl Popper: khoa học tiến bộ qua các cặp conjecture (phỏng đoán) — refutation (bác bỏ). AI agent đang biến agent thành \u0026ldquo;conjecture machine\u0026rdquo; — cỗ máy sản xuất phỏng đoán hàng loạt, rẻ. Nhưng refutation — bước kiểm chứng — vẫn mang tính vật lý và thể chế, nên vẫn chậm và tốn kém như cũ.\nToán học và khoa học máy tính là ngoại lệ thú vị vì có thể kiểm chứng in silico: agent sinh chứng minh, biểu diễn bằng ngôn ngữ hình thức (như Lean), rồi máy tính xác minh chắc chắn đúng/sai. DeepMind có công cụ Aletheia kết hợp bộ sinh chứng minh với một bộ xác minh ngôn ngữ tự nhiên gửi lỗi ngược lại để sửa. Trong thử thách \u0026ldquo;First Proof\u0026rdquo; đầu tiên (2/2026) với 10 bài toán chưa từng công bố, Aletheia giải được 6/10 — kết quả tốt nhất.\nNhưng đi kèm là một vấn đề mới: \u0026ldquo;proof indigestion\u0026rdquo; (khó tiêu hoá chứng minh) — cụm từ của Thang Luong, người dẫn dắt dự án Aletheia: \u0026ldquo;Chúng ta đang tiến tới một tương lai mà AI tạo ra đột phá nhanh hơn con người có thể review chúng.\u0026rdquo;\nỞ hầu hết các ngành khác (sinh học, y dược, vật liệu\u0026hellip;), khoảng cách kiểm chứng đang rộng ra chứ không thu hẹp lại. Agent có thể đề xuất một gene lead để đảo ngược lão hoá tế bào, nhưng không thể khẳng định nó thật sự hiệu quả. Đó là lý do các công ty như Google DeepMind, Ginkgo Bioworks, Lila Sciences đang đầu tư vào lab tự động hoá — nhưng công nghệ này còn đắt, còn sớm, và quan trọng hơn: tự động hoá không thể rút ngắn \u0026ldquo;đồng hồ sinh học\u0026rdquo; của tự nhiên. Dòng tế bào vẫn cần thời gian để lớn lên, phản ứng hoá học vẫn cần thời gian để hoàn tất. Cái lab, không phải cái model, mới là thứ quyết định tốc độ.\nBốn việc cần làm ngay, theo báo cáo Phần cuối báo cáo dành cho các nhà hoạch định chính sách và quỹ tài trợ khoa học, đề xuất 4 ưu tiên:\nĐảm bảo mọi nhà khoa học đều tiếp cận được agent tốt nhất — coi đây là ưu tiên chiến lược ngang với việc từng cấp quyền truy cập siêu máy tính trước đây. Điều thú vị: tranh luận địa chính trị hiện nay tập trung vào việc \u0026ldquo;quốc gia nào tự train được frontier model\u0026rdquo;, trong khi câu hỏi quan trọng hơn có thể là \u0026ldquo;quốc gia đó có triển khai được agent trên toàn bộ hệ sinh thái khoa học của mình hay không\u0026rdquo;. Biến dữ liệu quốc gia thành \u0026ldquo;agent-ready\u0026rdquo; — mở API có tài liệu tốt cho dữ liệu công khai/rủi ro thấp; xây giải pháp bảo mật quyền riêng tư cho dữ liệu nhạy cảm (gene, virus học\u0026hellip;) để agent truy cập an toàn, có audit. Giải quyết nút thắt kiểm chứng — đầu tư hạ tầng thí nghiệm sẵn có (cho thuê bench-time), đẩy nhanh lab tự động hoá. DeepMind đã mở một wet lab ngay trong Viện Francis Crick (Anh) để tài trợ nhà khoa học độc lập kiểm chứng giả thuyết do agent tạo ra. Trang bị agent cho người bình duyệt (peer reviewer) — vì bản thân giới khoa học cũng đang dùng AI để viết ngày càng nhiều đơn xin tài trợ và bài báo, khiến \u0026ldquo;chất lượng văn bản\u0026rdquo; không còn là thước đo đáng tin. Hội đồng Nghiên cứu Y khoa Anh (MRC) đã khôi phục lại vòng phỏng vấn cho ứng viên vào chung kết — một phản ứng khá \u0026ldquo;low-tech\u0026rdquo; trước vấn đề rất \u0026ldquo;high-tech\u0026rdquo;. Một câu mình muốn giữ lại Đoạn kết của báo cáo có một câu mình nghĩ đáng nhớ lâu:\nAI agents must not be viewed as a rationale to spend less on science; this would be a tragic false economy.\n(Tạm dịch: đừng biến sự xuất hiện của AI agent thành cái cớ để cắt giảm đầu tư cho khoa học — đó sẽ là một kiểu \u0026ldquo;tiết kiệm giả\u0026rdquo; đầy bi kịch.)\nĐọc xong báo cáo, điều mình rút ra không phải là \u0026ldquo;AI sắp làm khoa học thay con người\u0026rdquo;, mà là: cỗ máy tạo giả thuyết đang chạy nhanh hơn rất nhiều so với cỗ máy kiểm chứng giả thuyết — và khoảng cách đó, chứ không phải bản thân con AI, mới là thứ đáng để đầu tư giải quyết trong vài năm tới.\nBài viết này là tóm tắt và một vài suy nghĩ cá nhân sau khi đọc báo cáo \u0026ldquo;Conjecture Machines: AI agents and the new validation bottleneck in science\u0026rdquo; (Google DeepMind, tháng 7/2026). Các trích dẫn trong bài đều lấy từ báo cáo gốc.\n","permalink":"https://vo-hoang-kh4ng.github.io/posts/conjecture-machines-ai-agents-va-nut-that-kiem-chung-trong-khoa-hoc/","summary":"Tóm tắt và một vài suy nghĩ về báo cáo \u0026lsquo;Conjecture Machines\u0026rsquo; của Google DeepMind (7/2026): AI agent đang khiến việc tạo ra ý tưởng khoa học trở nên rẻ và dồi dào, nhưng khâu kiểm chứng lại chưa theo kịp.","title":"Conjecture Machines: AI agent và nút thắt kiểm chứng mới trong khoa học"},{"content":"Vì sao chọn Hugo? Build cực nhanh, không cần server, chỉ cần host file tĩnh. Viết bài bằng Markdown, quản lý bằng Git. Theme PaperMod tối giản, đọc dễ chịu — phong cách giống các blog kỹ thuật nổi tiếng như Lil\u0026rsquo;Log. Các bước chính hugo new site my-blog cd my-blog git init git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod Sau đó cấu hình hugo.toml, thêm bài viết mới trong content/posts/, và chạy thử:\nhugo server -D Deploy Dùng GitHub Actions để build và deploy tự động lên GitHub Pages mỗi khi push code lên nhánh main.\nVậy là xong — một blog cá nhân nhẹ, nhanh, miễn phí!\n","permalink":"https://vo-hoang-kh4ng.github.io/posts/thiet-lap-blog-voi-hugo/","summary":"Ghi chú nhanh các bước dựng blog tĩnh với Hugo, theme PaperMod, và deploy lên GitHub Pages.","title":"Thiết lập blog cá nhân với Hugo + PaperMod"},{"content":"Xin chào! Đây là bài viết đầu tiên trên blog cá nhân của mình.\nMình lập trang này để:\nGhi lại những gì học được trong quá trình code, tránh quên lãng. Chia sẻ các dự án nhỏ mình tự làm. Rèn thói quen viết lách và tư duy rõ ràng hơn. Blog được dựng bằng Hugo và theme PaperMod, host miễn phí trên GitHub Pages — cùng công nghệ với blog của Lilian Weng mà mình rất thích phong cách tối giản.\nHẹn gặp lại ở những bài viết tiếp theo!\n","permalink":"https://vo-hoang-kh4ng.github.io/posts/xin-chao/","summary":"Bài viết đầu tiên trên blog — lý do mình bắt đầu viết và dự định sắp tới.","title":"Xin chào, đây là blog của mình"},{"content":"Xin chào, mình là Kh4ng. 👋\nĐây là blog cá nhân nơi mình ghi lại những gì học được về lập trình, các dự án đang làm, và đôi khi là vài suy nghĩ vụn vặt trên đường đi.\nLiên hệ GitHub: vo-hoang-kh4ng ","permalink":"https://vo-hoang-kh4ng.github.io/about/","summary":"about","title":"Giới thiệu"},{"content":"Trang này gom các bài viết theo chủ đề, để dễ đọc theo mạch thay vì lướt theo thứ tự thời gian đăng như ở trang Bài viết. Mình sẽ cập nhật danh sách này khi có bài mới.\nBắt đầu từ đây Xin chào, đây là blog của mình Thiết lập blog cá nhân với Hugo + PaperMod AI \u0026amp; Khoa học Conjecture Machines: AI agent và nút thắt kiểm chứng mới trong khoa học Graph Engineering: Kiến trúc đồ thị cho hệ thống AI Agent thế hệ mới Fine-tuning LLM: Từ Prompt, RAG đến LoRA, QLoRA và DoRA Serving LLM: Vì sao chạy production khó hơn demo rất nhiều ","permalink":"https://vo-hoang-kh4ng.github.io/lo-trinh-doc/","summary":"Danh sách bài viết được sắp xếp theo chủ đề, thay vì theo thời gian đăng.","title":"Lộ trình đọc"}]