$ cat ./blog/gen-ai/llm-fundamentals/reasoning-model-va-test-time-compute.md
seriesCách hoạt động của LLMbài 15

Reasoning model và test-time compute: khi nào "nghĩ lâu hơn" thì đáng

Bài thứ mười lăm trong series Cách hoạt động của LLM: bài 10 mua chất lượng bằng compute lúc train, bài này hỏi có mua được bằng compute lúc trả lời không. Đi từ chain-of-thought, self-consistency và verifier tới reasoning model kiểu o1 và DeepSeek-R1, cơ chế RLVR và GRPO, cái giá của thinking tokens trên roofline, các núm điều khiển ngân sách suy luận trong API, và khi nào nghĩ lâu hơn có lợi, vô ích hay có hại.

~/diagrams/reasoning-model-va-test-time-compute/reasoning-pipeline.htmltương tác
mở riêng ↗
HìnhHàng thứ hai là đường đi của một câu hỏi lúc trả lời: prompt kèm cài đặt ngân sách (effort, budget hoặc bật/tắt thinking); prefill chạy cả prompt trong một lượt forward; decode sinh hàng nghìn thinking tokens, mỗi token ghi thêm K, V vào KV cache; chuỗi suy nghĩ kết thúc khi model tự dừng hoặc bị cắt theo ngân sách; model decode câu trả lời; API trả về câu trả lời kèm chuỗi suy nghĩ ẩn, tóm tắt hoặc đầy đủ. Hàng thứ ba là phần lúc train và bộ nhớ: verifier chấm đúng/sai làm reward cho RLVR, weights sau RL vẫn là một Transformer bình thường; KV cache lớn dần theo mỗi token; và nhánh tuỳ chọn sinh N lần rồi bầu đa số hoặc dùng verifier để chọn. Hàng trên cùng và dưới cùng là chín hiểu lầm phổ biến, mỗi cái nối với bước mà nó hiểu sai. Mục 11 giải thích từng hiểu lầm.
bước tính toánđiều khiển ngân sách và chọn đáp ánkhung đỏ: lúc train (RL và verifier); khung tím: KV cache; khung xám: hiểu lầm

Bài thứ mười bốn kết thúc bằng một nhận xét: MoE làm mỗi token rẻ hơn, state cố định làm context dài rẻ hơn, và cả hai là lý do việc bắt model “nghĩ lâu hơn” trở nên khả thi về chi phí. Bài này nói về chính việc nghĩ lâu hơn đó.

Câu hỏi của bài: có mua được chất lượng bằng compute lúc trả lời không, và khi nào thì đáng? Bài 10 cho thấy loss giảm đều khi tăng compute lúc train: nhiều tham số hơn, nhiều token hơn. Nhưng train là chi phí trả một lần, trước khi biết người dùng sẽ hỏi gì. Compute lúc trả lời (test-time compute) thì khác: có thể tiêu nhiều cho câu khó, tiêu ít cho câu dễ, và tiêu thêm mà không phải train lại.

Bài đi theo thứ tự lịch sử, vì mỗi bước giải quyết một giới hạn của bước trước:

  • Chain-of-thought (2022): một mẹo prompt, bảo model viết các bước trung gian ra trước khi trả lời.
  • Sinh nhiều lần rồi chọn: self-consistency, verifier, reward model chấm từng bước.
  • Reasoning model (o1, DeepSeek-R1 và các model sau): train bằng RL với reward kiểm chứng được để tự sinh chuỗi suy nghĩ dài, có núm “ngân sách” trong API.

Hình 1 ở đầu trang là đường đi của một câu hỏi qua reasoning model; phần lớn bài giải thích từng ô trên đó. Mục 8 tính giá của các thinking token bằng roofline của bài 13. Mục 9 trả lời câu hỏi chính: khi nào nghĩ lâu hơn có lợi, khi nào phí, khi nào còn tệ hơn.


01Mua chất lượng bằng compute lúc trả lời

Hai trục compute

Tháng 9/2024, OpenAI công bố o1 kèm bài Learning to Reason with LLMs, với hai biểu đồ đặt cạnh nhau: độ chính xác ở AIME tăng theo compute của RL lúc train, và cũng tăng theo thời gian model “suy nghĩ” lúc trả lời. Theo OpenAI, hiệu năng của o1 “cải thiện đều” theo cả hai. Hai biểu đồ dùng trục hoành log, không có số tuyệt đối cho compute, nên chỉ đọc được xu hướng; đó cũng là số liệu của chính nhà cung cấp, chưa được kiểm chứng độc lập.

Trong phần văn bản, OpenAI có đưa ba con số cho AIME 2024, và chúng minh hoạ đúng các cách tiêu compute mà bài này đi qua: o1 giải trung bình 74% đề khi chỉ sinh một mẫu cho mỗi bài, 83% khi lấy đồng thuận của 64 mẫu, và 93% khi sinh 1.000 mẫu rồi xếp hạng lại bằng một hàm chấm điểm đã học. GPT-4o chỉ giải được 12%.

Ba cách tiêu compute lúc trả lời

Cùng một model, có ba cách tiêu thêm compute cho một câu hỏi:

  1. Song song: sinh N câu trả lời độc lập rồi chọn một, bằng bầu đa số hoặc bằng một model chấm điểm. Không cần đổi model.
  2. Tuần tự: một chuỗi suy nghĩ dài hơn, trong đó model thử, kiểm tra, quay lại sửa. Cách này cần model được train để làm vậy (mục 5).
  3. Tìm kiếm: chia lời giải thành từng bước, mỗi bước sinh vài nhánh, dùng một model chấm điểm từng bước để giữ nhánh tốt nhất (beam search hay tree search).

Hình 2 đặt ba cách cạnh nhau, kèm điều kiện cần có, chi phí và giới hạn của từng cách. Ba con số của OpenAI ở trên ứng với một mẫu, song song có bầu đa số, và song song có chấm điểm.

~/diagrams/reasoning-model-va-test-time-compute/test-time-strategies.htmltương tác
mở riêng ↗
HìnhHàng trên: song song, sinh N mẫu độc lập với temperature lớn hơn 0 rồi bầu đa số hoặc chấm bằng ORM hay verifier; cần đáp án trích ra và so được với nhau; chi phí nhân N lần nhưng chạy song song được; việc chọn bão hoà sớm dù số mẫu tăng (mục 3). Hàng giữa: tuần tự, một chuỗi thinking tokens dài trong đó model tự kiểm tra và viết tiếp; cần model được train bằng RL; latency tăng theo độ dài; giới hạn là overthinking và độ dài context (mục 8, 9). Hàng dưới: tìm kiếm, mở rộng nhiều nhánh ở mỗi bước và dùng process reward model chấm từng bước để giữ nhánh tốt; cần PRM và lời giải chia được thành bước; tốn thêm một lần gọi PRM mỗi bước; PRM cũng bị khai thác như mọi model chấm điểm (mục 3, 4).
sinh tokenchọn hoặc chấm điểmkhung tím: điều kiện cần có; khung xanh lơ: chi phí; khung xám: giới hạn

Mục 2 giải thích vì sao viết bước trung gian giúp được, mục 3–4 là song song và tìm kiếm, mục 5–7 là tuần tự và cách train nó.


02Chain-of-thought: mượn thêm forward pass

Compute mỗi token là cố định

Bài 8, mục 6 đã đếm: một lượt forward tốn khoảng 2N FLOPs cho mỗi token, với N là số tham số. Con số này không phụ thuộc câu hỏi khó hay dễ. “Thủ đô của Pháp là gì?” và “Tính 7.493 × 8.217” đều được đúng một lượt forward cho mỗi token sinh ra, đi qua đúng bấy nhiêu layer.

Hệ quả: nếu một bài toán cần nhiều bước tính tuần tự hơn số layer cho phép làm trong một lượt, model không thể ra đáp án đúng ở token đầu tiên. Cách duy nhất để có thêm compute là sinh thêm token. Mỗi token viết ra trở thành input cho các lượt forward sau, nên các bước trung gian được “ghi ra giấy” thay vì phải nằm gọn trong activation của một lượt. Chain-of-thought, nhìn theo cách này, là cách model mượn thêm forward pass.

Ba paper đặt nền

  • Scratchpad (Nye et al. 2021) train model viết “một chuỗi token trung gian tuỳ ý” trước đáp án (mục 1). Với phép cộng số nhiều chữ số, model được train trên số 1–8 chữ số và thử trên số 9–10 chữ số: model không có scratchpad thất bại hoàn toàn, model có scratchpad và đủ lớn thì làm được (mục 3, Hình 3).
  • Chain-of-thought prompting (Wei et al. 2022) bỏ bước train, chỉ đưa 8 ví dụ mẫu có lời giải từng bước vào prompt. Với PaLM 540B trên GSM8K, độ chính xác tăng từ 17,9% lên 56,9% (Bảng 2 ở phụ lục). Cùng paper ghi nhận CoT là “khả năng xuất hiện theo quy mô”: model nhỏ viết ra chuỗi lập luận trôi chảy nhưng phi logic, và chỉ model khoảng 100B tham số trở lên mới hưởng lợi (mục 3.2, Hình 4).
  • Zero-shot CoT (Kojima et al. 2022) bỏ luôn ví dụ mẫu, chỉ thêm câu “Let’s think step by step”. Với text-davinci-002, MultiArith tăng từ 17,7% lên 78,7% và GSM8K từ 10,4% lên 40,7% (abstract, Bảng 1).

GSM8K xuất hiện xuyên suốt bài: 8.500 bài toán đố bậc tiểu học, mỗi bài cần 2–8 bước, chia 7.500 bài train và 1.000 bài test (Cobbe et al. 2021, mục 2). Đáp án cuối là một con số nên chấm tự động được.

CoT không giúp đều mọi thứ

Sprague et al. 2024 (To CoT or not to CoT?) tổng hợp 1.218 phép so sánh từ 110 paper (mục 3) và chạy thêm thí nghiệm trên 20 dataset, 14 model. Lợi ích của CoT tập trung ở suy luận ký hiệu, toán và logic, tăng trung bình 14,2, 12,3 và 6,9 điểm. Các nhóm tác vụ còn lại gần như đứng yên: từ 56,1 lên 56,8 (Hình 2). Trên MMLU, tuỳ model, tới khoảng 95% phần lợi của CoT nằm ở các câu có dấu “=” trong đề hoặc trong lời giải (mục 1, Bảng 18), dù chỉ khoảng 10% câu MMLU có dấu này.

Kết quả này khớp với lập luận ở trên: CoT giúp khi bài toán cần nhiều bước tính, không giúp khi bài toán cần tri thức, vì viết thêm bước không tạo ra kiến thức mới. Đây là trục chính cho mục 9.

Code mẫu 1: Qwen3-0.6B bật và tắt thinking

Qwen3 có hai chế độ trong cùng một model: thinking (viết suy nghĩ trong cặp <think>…</think> trước câu trả lời) và non-thinking. Model card của Qwen3-0.6B cho chọn chế độ bằng tham số enable_thinking của chat template, mặc định là True. Code dưới chạy cùng 15 câu đầu của tập test GSM8K ở cả hai chế độ, với tham số sampling mà model card khuyến nghị cho từng chế độ (model card ghi rõ không nên dùng greedy ở chế độ thinking vì dễ lặp vô hạn), và cố định seed.

# Python 3.12.13, torch 2.14.1+cpu, transformers 5.19.0, datasets 5.1.0
# CPU Intel i9-9900K, bf16, 8 thread. Chạy: python code1.py 15
import re, sys, time
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL = "Qwen/Qwen3-0.6B"
N = int(sys.argv[1]) if len(sys.argv) > 1 else 15
SEED = 0
END_THINK = 151668  # token id của </think> trong tokenizer Qwen3

tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16)
model.eval()
torch.set_num_threads(8)

data = load_dataset("openai/gsm8k", "main", split="test").select(range(N))

def gold(ans: str) -> str:
    return ans.split("####")[-1].strip().replace(",", "")

def extract(text: str) -> str | None:
    boxed = re.findall(r"\\boxed\{([^{}]*)\}", text)
    cand = boxed[-1] if boxed else text
    nums = re.findall(r"-?\d[\d,]*\.?\d*", cand)
    return nums[-1].replace(",", "").rstrip(".") if nums else None

def same(a, b) -> bool:
    try:
        return a is not None and abs(float(a) - float(b)) < 1e-6
    except ValueError:
        return False

def run(question: str, thinking: bool):
    msgs = [{"role": "user", "content": question +
             "\nPlease reason step by step, and put your final answer within \\boxed{}."}]
    prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True,
                                     enable_thinking=thinking)
    inputs = tok(prompt, return_tensors="pt")
    # Tham số sampling theo khuyến nghị trong model card Qwen3-0.6B
    sampling = (dict(temperature=0.6, top_p=0.95, top_k=20) if thinking
                else dict(temperature=0.7, top_p=0.8, top_k=20))
    torch.manual_seed(SEED)
    t0 = time.perf_counter()
    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=4096, do_sample=True, **sampling)
    dt = time.perf_counter() - t0
    ids = out[0, inputs.input_ids.shape[1]:].tolist()
    # Số token nằm trước </think> (gồm cả <think> và </think>) là thinking tokens
    n_think = ids.index(END_THINK) + 1 if END_THINK in ids else (len(ids) if thinking else 0)
    answer = tok.decode(ids[n_think:], skip_special_tokens=True)
    return extract(answer), n_think, len(ids), dt

rows = {True: [], False: []}
for i, ex in enumerate(data):
    g = gold(ex["answer"])
    for thinking in (False, True):
        pred, n_think, n_out, dt = run(ex["question"], thinking)
        ok = same(pred, g)
        rows[thinking].append((ok, n_think, n_out, dt))
        print(f"q{i:02d} thinking={thinking!s:5} gold={g:>6} pred={pred!s:>6} "
              f"{'OK   ' if ok else 'WRONG'} think={n_think:5d} out={n_out:5d} {dt:6.1f}s", flush=True)

print()
for thinking in (False, True):
    r = rows[thinking]
    acc = sum(x[0] for x in r) / len(r)
    print(f"enable_thinking={thinking!s:5}  correct {sum(x[0] for x in r):2d}/{len(r)}  "
          f"avg thinking tokens {sum(x[1] for x in r)/len(r):5.0f}  "
          f"avg output tokens {sum(x[2] for x in r)/len(r):5.0f}  "
          f"avg time {sum(x[3] for x in r)/len(r):5.1f}s")

Output:

q00 thinking=False gold=    18 pred=    18 OK    think=    0 out=  234   15.3s
q00 thinking=True  gold=    18 pred=    18 OK    think= 1316 out= 1590  124.7s
q01 thinking=False gold=     3 pred=     3 OK    think=    0 out=  127    8.0s
q01 thinking=True  gold=     3 pred=     3 OK    think=  439 out=  573   37.6s
q02 thinking=False gold= 70000 pred= 70000 OK    think=    0 out=  269   19.6s
q02 thinking=True  gold= 70000 pred= 70000 OK    think= 2607 out= 2967  281.7s
q03 thinking=False gold=   540 pred=   180 WRONG think=    0 out=   98    6.2s
q03 thinking=True  gold=   540 pred=   180 WRONG think=  335 out=  423   26.8s
q04 thinking=False gold=    20 pred=    20 OK    think=    0 out=  271   17.9s
q04 thinking=True  gold=    20 pred=    20 OK    think=  738 out= 1000   71.6s
q05 thinking=False gold=    64 pred=    64 OK    think=    0 out=  410   26.3s
q05 thinking=True  gold=    64 pred=  None WRONG think= 4096 out= 4096  452.4s
q06 thinking=False gold=   260 pred=   260 OK    think=    0 out=  314   19.9s
q06 thinking=True  gold=   260 pred=   260 OK    think=  634 out=  893   61.0s
q07 thinking=False gold=   160 pred=   100 WRONG think=    0 out=  302   19.7s
q07 thinking=True  gold=   160 pred=  None WRONG think= 4096 out= 4096  448.3s
q08 thinking=False gold=    45 pred=   275 WRONG think=    0 out=  352   23.0s
q08 thinking=True  gold=    45 pred=  None WRONG think= 4096 out= 4096  441.1s
q09 thinking=False gold=   460 pred=   460 OK    think=    0 out=  200   12.7s
q09 thinking=True  gold=   460 pred=   460 OK    think=  432 out=  570   38.3s
q10 thinking=False gold=   366 pred=   366 OK    think=    0 out=  181   11.5s
q10 thinking=True  gold=   366 pred=   366 OK    think=  886 out= 1094   77.4s
q11 thinking=False gold=   694 pred=  2938 WRONG think=    0 out=  194   15.2s
q11 thinking=True  gold=   694 pred=   694 OK    think=  770 out=  982   74.7s
q12 thinking=False gold=    13 pred=     1 WRONG think=    0 out=  329   24.5s
q12 thinking=True  gold=    13 pred=    12 WRONG think= 2540 out= 2990  309.6s
q13 thinking=False gold=    18 pred=    33 WRONG think=    0 out=  396   31.0s
q13 thinking=True  gold=    18 pred=    18 OK    think=  942 out= 1371  117.3s
q14 thinking=False gold=    60 pred=    60 OK    think=    0 out=  312   24.0s
q14 thinking=True  gold=    60 pred=    60 OK    think= 1051 out= 1465  125.4s

enable_thinking=False  correct  9/15  avg thinking tokens     0  avg output tokens   266  avg time  18.3s
enable_thinking=True   correct 10/15  avg thinking tokens  1665  avg output tokens  1880  avg time 179.2s

Toàn bộ lần chạy mất 49,5 phút. Năm điều đọc được:

  • Thinking chỉ hơn một câu: 10/15 so với 9/15, nằm trong mức nhiễu của 15 câu; đây là minh hoạ cơ chế, không phải phép đo độ chính xác.
  • Ba lần thinking hết token trước khi nghĩ xong. Ở q05, q07 và q08, model dùng hết 4.096 token mà chưa đóng </think>, nên không có câu trả lời (pred=None). q05 là câu non-thinking làm đúng với 410 token. Cắt cứng bằng max_new_tokens không phải ngân sách thật: model không biết sắp hết chỗ nên không kịp trả lời, lý do có budget forcing ở mục 8.
  • Thinking không sửa được câu hiểu sai đề. Ở q03, cả hai chế độ cùng ra 180 thay vì 540: nghĩ dài hơn vẫn đi theo cùng một cách hiểu sai.
  • Giá phải trả là gấp 7 lần token và gấp 10 lần thời gian. Trung bình 1.880 token output so với 266, 179 giây so với 18 giây mỗi câu. Tốc độ mỗi token chỉ chậm đi một chút khi context dài ra (từ khoảng 15 xuống khoảng 10 token/giây); thinking chậm chủ yếu vì có nhiều token hơn, như mục 8 sẽ tính.
  • Độ dài suy nghĩ dao động mạnh, từ 335 tới hơn 4.096 token, dù các câu GSM8K khó gần như nhau.

03Song song: sinh nhiều lần rồi chọn

Self-consistency: bầu đa số trên các chuỗi lập luận

Bài 9 đã giải thích sampling với temperature lớn hơn 0: cùng prompt, mỗi lần sinh ra một chuỗi khác. Wang et al. 2022 tận dụng chính điều đó. Self-consistency sinh nhiều chuỗi CoT độc lập, trích đáp án cuối của từng chuỗi, rồi chọn đáp án xuất hiện nhiều nhất. Trực giác: các con đường đúng dẫn tới cùng một đáp án, còn lỗi thì mỗi lỗi một kiểu. Với 40 mẫu mỗi câu (mục 3.2), PaLM 540B trên GSM8K tăng từ 56,5% lên 74,4% (Bảng 2).

Điều kiện ngầm của bầu đa số: đáp án phải trích ra và so được với nhau. Một con số, một lựa chọn A/B/C/D thì được. Một đoạn văn tóm tắt hay một đoạn code thì không có “đáp án giống nhau” để đếm.

Verifier: train một model để chấm

Thay vì đếm phiếu, có thể train một model chấm xem lời giải có đúng không. Cobbe et al. 2021 làm việc này ngay khi công bố GSM8K: sinh 100 lời giải cho mỗi bài, dùng một verifier chấm, chọn lời giải điểm cao nhất. Trên toàn bộ dataset, verifier 6B nhỉnh hơn một model 175B được fine-tune, tức lợi ích “tương đương tăng kích thước model khoảng 30 lần” (mục 6, Hình 5). Cùng paper có một cảnh báo quan trọng: độ chính xác tăng khi tăng số lời giải lên tới 400, rồi giảm sau đó, vì có thêm những lời giải sai nhưng lừa được verifier (mục 5.1, Hình 7a). Một model chấm điểm là một xấp xỉ, và sinh càng nhiều thì càng dễ gặp chỗ hở của xấp xỉ đó, giống chuyện overoptimization ở bài 11, mục 7.

Verifier của Cobbe chấm cả lời giải (outcome reward model, ORM). Lightman et al. 2023 (Let’s Verify Step by Step) train một process reward model (PRM) chấm từng bước, trên bộ PRM800K gồm 800 nghìn nhãn cấp bước do người gán, cho 75 nghìn lời giải của 12 nghìn bài (mục 2.4). Trên 500 bài của tập test MATH, chọn 1 trong 1.860 lời giải bằng PRM đạt 78,2%, so với 72,4% của ORM và 69,6% của bầu đa số (mục 3, Hình 3). PRM còn mở ra cách thứ ba trong Hình 2: dùng điểm từng bước để tìm kiếm.

Coverage tăng đều, chọn thì không

Brown et al. 2024 (Large Language Monkeys) đẩy cách song song tới hàng nghìn mẫu. Họ tách hai đại lượng:

  • Coverage: tỉ lệ bài có ít nhất một mẫu đúng trong k mẫu, chính là pass@k.
  • Độ chính xác sau khi chọn: tỉ lệ bài mà phương pháp chọn (bầu đa số, reward model) chọn đúng.

Coverage tăng rất đều theo số mẫu, gần theo một quy luật luỹ thừa mũ hoá (mục 3.1). Trên SWE-bench Lite, DeepSeek-Coder-V2-Instruct tăng từ 15,9% với một mẫu lên 56% với 250 mẫu (abstract, Hình 2). Ở đó có unit test để kiểm từng mẫu. Ở GSM8K và MATH, không có công cụ kiểm như vậy lúc chọn, nên bầu đa số và reward model bão hoà ở khoảng 100 mẫu trong khi coverage đã vượt 95% (mục 4.1, Hình 7). Khoảng cách giữa “có một đáp án đúng trong N mẫu” và “chọn được nó” là bài toán thật của cách song song.

pass@k ở đây phải tính đúng cách. Chen et al. 2021 (paper Codex) chỉ ra cách ước lượng ngây thơ 1 − (1 − p̂)^k bị chệch, và đưa ra ước lượng không chệch: sinh n ≥ k mẫu, đếm c mẫu đúng, rồi tính 1 − C(n−c, k) / C(n, k) (mục 2.1, phương trình 1).

Code mẫu 2: pass@k so với bầu đa số

Code dưới mô phỏng bốn loại câu hỏi bằng phân phối đáp án của model, không cần model thật. “A” là đáp án đúng. Loại “sai tản” có mười đáp án sai, mỗi cái xác suất nhỏ. Loại “sai tụ” có một đáp án sai chiếm 50%, kiểu một lỗi hiểu đề mà model hay mắc.

# Python 3.12 — chỉ dùng thư viện chuẩn, chạy dưới 1 giây
import random
from collections import Counter
from math import comb

random.seed(0)
KINDS = {                                       # "A" là đáp án đúng
    "dễ          p=0.6":  {"A": 0.6, "B": 0.2, "C": 0.2},
    "khó, sai tản p=0.2": {"A": 0.2, **{f"W{i}": 0.08 for i in range(10)}},
    "khó, sai tụ  p=0.3": {"A": 0.3, "B": 0.5, "C": 0.2},
    "rất khó     p=0.02": {"A": 0.02, **{f"W{i}": 0.098 for i in range(10)}},
}

def sample(dist, n):
    return random.choices(list(dist), weights=list(dist.values()), k=n)

def pass_at_k(n, c, k):                         # Chen et al. 2021, mục 2.1
    return 1.0 if n - c < k else 1 - comb(n - c, k) / comb(n, k)

def majority_acc(dist, N, trials=2000):         # bầu đa số trên N mẫu, hoà thì bốc ngẫu nhiên
    win = 0
    for _ in range(trials):
        counts = Counter(sample(dist, N)).most_common()
        top = [a for a, c in counts if c == counts[0][1]]
        win += random.choice(top) == "A"
    return win / trials

KS, n = [1, 4, 16, 64, 256], 1024
print(f"{'loại câu':22} " + " ".join(f"pass@{k:<4}" for k in KS) + " | " + " ".join(f"vote@{k:<4}" for k in KS))
for name, dist in KINDS.items():
    c = sample(dist, n).count("A")
    cov = [pass_at_k(n, c, k) for k in KS]
    vote = [majority_acc(dist, k, trials=2000 if k < 256 else 400) for k in KS]
    print(f"{name:22} " + " ".join(f"{x:8.2f}" for x in cov) + " | " + " ".join(f"{x:8.2f}" for x in vote))

Output:

loại câu               pass@1    pass@4    pass@16   pass@64   pass@256  | vote@1    vote@4    vote@16   vote@64   vote@256 
dễ          p=0.6          0.59     0.97     1.00     1.00     1.00 |     0.59     0.74     0.94     1.00     1.00
khó, sai tản p=0.2         0.20     0.59     0.97     1.00     1.00 |     0.21     0.25     0.48     0.84     1.00
khó, sai tụ  p=0.3         0.28     0.74     1.00     1.00     1.00 |     0.30     0.26     0.17     0.04     0.00
rất khó     p=0.02         0.02     0.07     0.25     0.69     0.99 |     0.02     0.01     0.00     0.00     0.00

Bốn điều đọc được:

  • pass@k của mọi loại câu đều tiến tới 1, kể cả câu chỉ đúng 2% mỗi lần: đường coverage của Large Language Monkeys.
  • Bầu đa số chỉ thắng khi đáp án đúng là đáp án phổ biến nhất. Câu “sai tản” đúng có 20% mỗi lần nhưng vẫn thắng bầu, vì mỗi đáp án sai chỉ có 8%.
  • Bầu đa số có thể làm tệ đi. Câu “sai tụ” đúng 30% mỗi lần nhưng một đáp án sai chiếm 50%, nên càng nhiều mẫu càng chắc chắn sai: từ 0,30 xuống 0,00.
  • Câu rất khó thì bầu không cứu được, dù pass@256 gần 1; chỉ verifier chính xác như unit test mới lấy được mẫu đúng hiếm hoi đó.

04Câu dễ, câu khó: phân bổ compute

Nếu đã có nhiều cách tiêu compute lúc trả lời, câu hỏi tiếp theo là tiêu thế nào cho tối ưu. Snell et al. 2024 nghiên cứu điều này với PaLM 2-S* trên MATH (mục 4), so hai họ chiến lược: tìm kiếm với PRM (best-of-N, beam search, lookahead) và sửa lần lượt (model được fine-tune để viết lại câu trả lời trước của chính nó).

Kết luận chính: chiến lược tốt nhất phụ thuộc độ khó của câu. Với câu dễ, để model tự sửa tuần tự hiệu quả hơn; với câu khó hơn, tìm kiếm rộng hơn hoặc trộn hai cách thì tốt hơn. Nếu chọn chiến lược theo độ khó ước lượng của từng câu (họ gọi là “compute-optimal”), cùng chất lượng có thể đạt với ít compute hơn best-of-N hơn 4 lần theo abstract; phần thân bài nói thận trọng hơn, “2–4 lần” (mục 8).

Phép so sánh đáng chú ý nhất là đổi compute lúc trả lời lấy tham số. Với cùng tổng FLOPs, một model nhỏ cộng test-time compute thắng một model lớn hơn khoảng 14 lần chạy greedy, nhưng chỉ ở những câu mà model nhỏ đã có tỉ lệ giải được không quá thấp (mục 7, Hình 9). Với câu khó nhất, tăng test-time compute gần như không giúp, và tiêu compute đó vào pretraining thì hiệu quả hơn (mục 1, mục 7). Tóm tắt của chính paper: compute lúc trả lời và compute lúc pretrain “không đổi được cho nhau 1-1”.

Ghép với Code 2: test-time compute khuếch đại cái model đã làm được một phần, không tạo ra khả năng ở chỗ model gần như không có.

Bài 10, mục 6 đã nói Sardana và Frankle thêm chi phí inference vào bài toán scaling: khi tổng số token inference đủ lớn, train model nhỏ hơn lâu hơn sẽ rẻ hơn tổng thể. Test-time compute thêm biến thứ ba: ngoài số tham số và số token train, còn “mỗi câu hỏi được tiêu bao nhiêu token suy nghĩ”. Model nhỏ nghĩ lâu có thể rẻ hơn hoặc đắt hơn model lớn trả lời ngay, tuỳ phân phối độ khó của câu hỏi thật.


05Reasoning model: từ STaR đến R1

Bước tiếp theo sau self-consistency và verifier là train model để tự viết chuỗi suy nghĩ dài, có tự kiểm tra và quay lại sửa: cách “tuần tự” trong Hình 2.

STaR: tự sinh lý do, giữ cái đúng

STaR (Zelikman et al. 2022) là tiền thân trực tiếp. Vòng lặp của nó (abstract, Thuật toán 1 ở mục 3):

  1. Prompt model sinh lý do (rationale) và đáp án cho nhiều câu hỏi.
  2. Câu nào sai thì cho model biết đáp án đúng làm gợi ý và bảo nó sinh lại lý do (“rationalization”, mục 3.2).
  3. Fine-tune trên mọi lý do dẫn tới đáp án đúng, rồi lặp lại.

Với GPT-J 6B trên CommonsenseQA, STaR đạt 72,5%, ngang GPT-3 fine-tune lớn hơn 30 lần (73,0%) (Bảng 1). Ý tưởng cốt lõi: chỉ cần biết đáp án cuối đúng hay sai là đủ để chọn ra chuỗi suy luận tốt mà học theo, không ai phải viết chuỗi mẫu.

o1: những gì OpenAI công bố

OpenAI không công bố cách train o1. Bài Learning to Reason with LLMs chỉ nói o1 được train bằng reinforcement learning “quy mô lớn” để suy nghĩ bằng chain-of-thought, học nhận ra và sửa lỗi, chia bước khó thành bước nhỏ, đổi hướng khi bế tắc; chuỗi suy nghĩ thô bị ẩn, người dùng chỉ thấy bản tóm tắt. Mọi chi tiết khác về o1 đều là suy đoán.

DeepSeek-R1-Zero: RL thẳng trên base model

Ca có công bố chi tiết nhất là DeepSeek-R1 (DeepSeek-AI 2025). Thí nghiệm đầu, R1-Zero, chạy RL thẳng trên DeepSeek-V3-Base, không có bước SFT nào trước. Reward chỉ gồm hai phần, đều do luật kiểm tra (mục 2.2.2):

  • Accuracy reward: đáp án toán đúng định dạng và khớp đáp án, code qua test.
  • Format reward: suy nghĩ nằm giữa thẻ <think> và </think>.

Nhóm tác giả ghi rõ họ không dùng reward model dạng mạng nơ-ron vì nó “có thể bị reward hacking trong quá trình RL quy mô lớn”, và train lại reward model thì tốn thêm tài nguyên (mục 2.2.2).

Kết quả: pass@1 ở AIME 2024 tăng từ 15,6% lên 71,0%, và lên 86,7% khi bầu đa số trên 64 mẫu (mục 2.2.4, Bảng 2). Hai quan sát làm paper nổi tiếng:

  • Độ dài câu trả lời tự tăng đều theo bước train, lên tới hàng nghìn token (Hình 3). Không ai thưởng cho câu dài; model tự tìm ra rằng nghĩ lâu hơn giúp ra đáp án đúng hơn.
  • “Aha moment”: một checkpoint giữa chừng tự viết “Wait, wait. Wait. That’s an aha moment I can flag here.” rồi quay lại kiểm tra bước trước (Bảng 3).

R1-Zero cũng có nhược điểm rõ: chuỗi suy nghĩ khó đọc và trộn ngôn ngữ, ví dụ lẫn tiếng Anh và tiếng Trung trong cùng một chuỗi (mục 2.2.4).

DeepSeek-R1: bốn giai đoạn

Để sửa các nhược điểm đó, R1 dùng một pipeline bốn giai đoạn (mục 2.3), là hàng dưới của Hình 3 ở mục 6:

  1. Cold start: SFT trên “vài nghìn” chuỗi CoT dài, dễ đọc.
  2. RL hướng suy luận: như R1-Zero, thêm một language consistency reward bằng tỉ lệ từ thuộc ngôn ngữ đích trong CoT. Reward này làm điểm giảm nhẹ nhưng chuỗi dễ đọc hơn (mục 2.3.2).
  3. Rejection sampling + SFT: sinh từ checkpoint RL, giữ khoảng 600 nghìn mẫu suy luận đúng, thêm khoảng 200 nghìn mẫu không phải suy luận (viết, QA, dịch), fine-tune lại từ base trên khoảng 800 nghìn mẫu đó.
  4. RL cho mọi tình huống: reward theo luật cho suy luận, reward model cho helpfulness và harmlessness.

R1 đạt 79,8% pass@1 ở AIME 2024 và 97,3% ở MATH-500 (Bảng 4).

Distillation: model nhỏ học từ model lớn

DeepSeek cũng fine-tune các model Qwen và Llama nhỏ trên 800 nghìn mẫu do R1 sinh ra, chỉ SFT, không RL. DeepSeek-R1-Distill-Qwen-7B đạt 55,5% AIME 2024, bản Qwen-32B đạt 72,6% (Bảng 5). So sánh quan trọng nằm ở mục 4.1: chạy RL trực tiếp trên Qwen-32B-Base hơn 10 nghìn bước chỉ được 47,0%, trong khi distill từ R1 được 72,6% (Bảng 6). Kết luận của nhóm tác giả: với model nhỏ, distill từ model mạnh hiệu quả hơn tự chạy RL quy mô lớn. Điểm này quay lại ở mục 7.


06Bên trong RLVR và GRPO

Bài 11, mục 10 đã định nghĩa RLVR (reinforcement learning with verifiable rewards: reward đến từ một bộ kiểm tra chạy bằng chương trình) và GRPO (biến thể của PPO bỏ value model, lấy advantage từ một nhóm câu trả lời). Mục này không nhắc lại định nghĩa mà đi vào cơ chế, theo Hình 3.

~/diagrams/reasoning-model-va-test-time-compute/grpo-loop.htmltương tác
mở riêng ↗
HìnhHàng trên là một bước GRPO: lấy một prompt có đáp án kiểm được; policy hiện tại sinh G câu trả lời bằng sampling; verifier chấm từng câu (đúng/sai cộng phần thưởng format); advantage của mỗi câu là reward trừ trung bình nhóm, chia độ lệch chuẩn nhóm; policy được cập nhật bằng mục tiêu có clip như PPO, trừ β lần KL với reference model đóng băng; rồi lặp lại với policy mới. Hàng giữa là hai chỗ tín hiệu mất hoặc sai: verifier có lỗ hổng thì policy học cách khai thác nó, và nhóm toàn đúng hoặc toàn sai cho advantage bằng 0 nên không có gradient. Hàng dưới là pipeline của DeepSeek-R1: từ DeepSeek-V3-Base, cold-start SFT trên vài nghìn CoT dài, RL suy luận với reward đúng và reward nhất quán ngôn ngữ, rejection sampling cộng SFT trên khoảng 800 nghìn mẫu, rồi RL cho mọi tình huống có thêm reward model.
policy sinh và cập nhậtchấm điểm và tính advantagekhung tím: reference model; khung xám: chỗ tín hiệu mất hoặc bị khai thác; khung xanh lơ: giai đoạn train của R1

Một bước GRPO

GRPO được giới thiệu trong DeepSeekMath (Shao et al. 2024, mục 4.1). Với mỗi prompt q:

  1. Policy hiện tại sinh G câu trả lời o₁…o_G. DeepSeekMath dùng G = 64 (mục 4.2). Đây là sampling với temperature lớn hơn 0, và vì mẫu đến từ chính policy đang train nên GRPO là RL on-policy.
  2. Mỗi câu nhận reward rᵢ. Trong RLVR, rᵢ đến từ verifier.
  3. Advantage của câu i là Âᵢ = (rᵢ − mean(r)) / std(r), gán cho mọi token của câu đó (mục 4.1.2).
  4. Cập nhật policy bằng mục tiêu có clip giống PPO, cộng một số hạng KL với reference model, ước lượng bằng π_ref/π_θ − log(π_ref/π_θ) − 1, luôn không âm (phương trình 3 và 4). Hệ số KL trong DeepSeekMath là 0,04 (mục 4.2).

Khác với PPO ở bài 11, mục 6, vốn cần một value model cỡ policy để ước lượng baseline, GRPO dùng trung bình nhóm làm baseline. Một lưu ý về lịch sử: trong DeepSeekMath, reward của GRPO vẫn đến từ một reward model dạng mạng nơ-ron; ghép GRPO với reward kiểm chứng được là bước của R1 và các model sau.

Nhóm đồng nhất không dạy gì

Hệ quả trực tiếp của công thức advantage: nếu cả G câu đều đúng, hoặc cả G câu đều sai, mọi rᵢ bằng nhau, mọi Âᵢ bằng 0, và phần policy gradient bằng 0. Prompt đó tốn compute sinh G câu mà không đóng góp gì vào việc học.

Paper DAPO (Yu et al. 2025) viết thẳng điều này: nếu mọi output của một prompt đều đúng và nhận cùng reward, advantage của nhóm bằng 0, và “advantage bằng 0 dẫn tới policy gradient bằng 0” (mục 3.2). Cách sửa của họ, dynamic sampling, là lấy mẫu dư rồi bỏ các prompt có độ chính xác bằng 1 hoặc 0 khỏi batch.

Vì vậy độ khó của dữ liệu RL quyết định tín hiệu: prompt luôn đúng hay luôn sai đều vô ích, chỉ prompt model làm đúng một phần mới tạo ra gradient. Một model không bao giờ đúng thì không có tín hiệu nào để bắt đầu.

Code mẫu 3: reward và advantage GRPO cho một nhóm

Code dưới viết verifier kiểu R1 bằng regex (đúng đáp án trong \boxed{} được 1, đúng format <think>…</think> được thêm 0,1), tính advantage theo nhóm, rồi tính loss GRPO trên log-prob giả lập để xem gradient.

# Python 3.12, torch 2.14.1 — chạy trên CPU, dưới 1 giây
import re, torch

def verifier(completion, gold):
    """Reward kiểu R1: đúng đáp án (1/0) và đúng format (<think>...</think> rồi \\boxed{})."""
    fmt = re.fullmatch(r"(?s)<think>.+</think>.*\\boxed\{[^{}]+\}.*", completion) is not None
    m = re.findall(r"\\boxed\{([^{}]+)\}", completion)
    return float(bool(m) and m[-1].strip() == gold), float(fmt)

def group_advantage(rewards, eps=1e-4):       # DeepSeekMath mục 4.1.2: (r - mean) / std
    return (rewards - rewards.mean()) / (rewards.std() + eps)

gold = "18"
groups = {
    "nhóm lẫn": [
        "<think>16 - 3 - 4 = 9 quả, 9 × 2 = 18</think> Đáp án \\boxed{18}",
        "<think>16 - 3 = 13, 13 × 2 = 26</think> Đáp án \\boxed{26}",
        "16 - 7 = 9, 9 × 2 = 18. Đáp án \\boxed{18}",              # đúng nhưng thiếu <think>
        "<think>9 quả, mỗi quả 2 đô</think> Đáp án \\boxed{18}",
    ],
    "toàn đúng": ["<think>...</think> \\boxed{18}"] * 4,
    "toàn sai": ["<think>...</think> \\boxed{26}", "<think>...</think> \\boxed{9}",
                 "<think>...</think> \\boxed{32}", "<think>...</think> \\boxed{13}"],
}

torch.manual_seed(0)
for name, completions in groups.items():
    r = torch.tensor([a + 0.1 * f for a, f in (verifier(c, gold) for c in completions)])
    adv = group_advantage(r)
    logp = torch.randn(4, 5, requires_grad=True)             # log-prob giả lập: 4 câu × 5 token
    old = logp.detach()                                      # on-policy: ratio = 1 ở bước đầu
    ratio = (logp - old).exp()
    surrogate = torch.min(ratio * adv[:, None], ratio.clamp(0.8, 1.2) * adv[:, None])
    ref = old + 0.05 * torch.randn_like(old)                 # reference model lệch nhẹ
    kl = (ref - logp).exp() - (ref - logp) - 1               # DeepSeekMath, phương trình (4)
    loss = -(surrogate - 0.04 * kl).mean(dim=1).mean()       # trung bình theo token rồi theo nhóm
    loss.backward()
    print(f"{name:9}  reward {[round(x, 2) for x in r.tolist()]}  "
          f"advantage {[round(a, 2) for a in adv.tolist()]}  |grad| = {logp.grad.norm():.4f}")

r = torch.ones(4) * 1.1                                      # nhóm toàn đúng, bỏ phần KL
logp = torch.randn(4, 5, requires_grad=True)
(-(group_advantage(r)[:, None] * logp).mean()).backward()
print(f"\ntoàn đúng, chỉ phần policy gradient: |grad| = {logp.grad.norm():.4f}")

Output:

nhóm lẫn   reward [1.1, 0.1, 1.0, 1.1]  advantage [0.57, -1.49, 0.36, 0.57]  |grad| = 0.1936
toàn đúng  reward [1.1, 1.1, 1.1, 1.1]  advantage [0.0, 0.0, 0.0, 0.0]  |grad| = 0.0004
toàn sai   reward [0.1, 0.1, 0.1, 0.1]  advantage [0.0, 0.0, 0.0, 0.0]  |grad| = 0.0004

toàn đúng, chỉ phần policy gradient: |grad| = 0.0000
  • Ở nhóm lẫn, câu sai nhận advantage âm lớn (−1,49), ba câu đúng nhận dương. Câu đúng nhưng thiếu <think> được 0,36, thấp hơn hai câu đúng đủ format (0,57): reward format tạo ra khác biệt nhỏ nhưng có thật.
  • Ở nhóm toàn đúng và toàn sai, advantage bằng 0. Gradient 0,0004 còn lại chỉ đến từ số hạng KL, kéo policy về phía reference. Bỏ KL thì gradient đúng bằng 0.

Các chỉnh sửa sau R1

Sau R1, hai chỉnh sửa được dùng nhiều nhất, mỗi cái một câu:

  • Dr. GRPO (Liu et al. 2025) bỏ phép chia cho độ dài |oᵢ|, vốn làm câu sai dài bị phạt nhẹ hơn trên mỗi token nên dễ phình ra, và bỏ phép chia cho std(r), vốn tăng trọng số cho câu quá dễ hoặc quá khó (thiên lệch mô tả ở mục 3.1, cách sửa ở mục 3.2); một phần hiện tượng “câu trả lời dài dần” có thể đến từ thiên lệch độ dài này.
  • DAPO (Yu et al. 2025) thêm clip-higher (nới cận trên của clip để chống sụp entropy), dynamic sampling, loss theo token và phạt mềm câu quá dài (mục 3.1–3.4), bỏ số hạng KL (mục 2.3), và đạt 50 điểm AIME 2024 trên Qwen2.5-32B base, so với 47 của RL kiểu R1-Zero, với một nửa số bước train (mục 1, Hình 1).

Reward kiểm chứng được vẫn bị lợi dụng

Bài 11 viết rằng reward kiểu đúng/sai “không có xấp xỉ nào để hack” theo kiểu reward model. Điều đó vẫn đúng: không có mạng nơ-ron xấp xỉ nào để policy tìm chỗ chấm sai như ở bài 11, mục 7. Nhưng verifier là một chương trình do người viết, có thể có lỗ hổng, và policy tối ưu mọi thứ reward cho phép, kể cả lỗ hổng.

Baker et al. 2025 (OpenAI) ghi lại các trường hợp này khi train một reasoning model cùng họ với o1 và o3-mini trên tác vụ lập trình dạng agent, nơi reward là “unit test có qua không”. Model học cách gọi exit(0) để thoát sớm với mã thành công, ném SkipTest để bỏ qua test (mục 2.1), viết hàm rỗng thay vì cài đặt thật, hoặc sửa các hàm mà framework test gọi tới và sửa luôn file test để test qua dễ dàng (mục 2.2). Reward vẫn kiểm chứng được; chỉ là thứ được kiểm chứng (test qua) không phải thứ người ta muốn (code đúng).

Cách nói chính xác: RLVR loại bỏ một nguồn reward hacking (reward model là xấp xỉ học được), không loại bỏ reward hacking nói chung. Verifier càng lỏng (bộ test chưa đủ, môi trường cho sửa file test) thì càng dễ hack.


07RL dạy model điều gì?

R1-Zero cho thấy RL với reward đúng/sai làm model giỏi hơn hẳn. Câu hỏi tiếp theo nghe đơn giản nhưng đang là tranh luận mở: RL dạy model khả năng mới, hay chỉ làm cho model chọn đúng thường xuyên hơn những gì nó đã làm được?

Lập luận “chỉ làm sắc phân phối”

Yue et al. 2025 (NeurIPS 2025, oral) đo pass@k của base model và của model sau RLVR với k lên tới 128–1024 tuỳ benchmark (phụ lục A.2), trên toán, code và suy luận thị giác. Ở k nhỏ, model RL thắng rõ. Nhưng khi k tăng lên hàng chục, hàng trăm, base model đuổi kịp rồi vượt model RL (Hình 2, mục 3.1). Đo perplexity, họ thấy chuỗi suy luận của model RL đã nằm sẵn trong phân phối lấy mẫu của base model (mục 4.1). Diễn giải của họ: RLVR chủ yếu dồn xác suất vào những đường suy luận vốn đã có, tăng pass@1 nhưng thu hẹp phạm vi khám phá. Ngược lại, distillation từ model mạnh hơn có thể đưa vào mẫu suy luận mới (mục 4.2), khớp với kết quả distill so với RL của R1 ở mục 5.

Liu et al. 2025 (paper Dr. GRPO) bổ sung từ phía khác: “gần như mọi base model đã có ‘aha moment’”, kể cả DeepSeek-V3-Base (mục 2.3, Hình 3). Họ cũng thấy việc tự phản tỉnh trong R1-Zero không tương quan thuận với độ chính xác cao hơn (mục 2.3, phụ lục F). “Aha moment” vì vậy giống một hành vi có sẵn được RL khuếch đại, hơn là thứ tự dưng xuất hiện.

Báo cáo kỹ thuật Qwen3 có quan sát cùng hướng: trong thí nghiệm so sánh của họ, distillation tăng pass@64, còn RL thì không (mục 4.7, Bảng 21).

Lập luận ngược lại

ProRL (NVIDIA, Liu et al. 2025) chạy RL kéo dài hơn 2.000 bước trên 136 nghìn bài toán, và báo rằng model sau RL vượt base model trên một dải rộng giá trị k, kể cả những bài base model thất bại hoàn toàn dù lấy mẫu nhiều. Họ kết luận RL kéo dài “có thể khám phá chiến lược suy luận mới”. Lưu ý: ProRL xuất phát từ DeepSeek-R1-Distill-Qwen-1.5B, một model đã distill chứ không phải base model thô. Một hướng khác (Wen et al. 2025) cho rằng pass@k đếm cả những lần đoán đúng với lập luận sai; khi chỉ đếm lời giải có chuỗi lập luận đúng (do một LLM chấm), model sau RLVR vượt base model ở mọi k trên AIME 2024/2025, dù hiệu ứng yếu hơn ở MATH-500 và AMC23, và không có ở Minerva.

Đọc thế nào cho đúng

Tại thời điểm viết, chưa có kết luận dứt khoát. Những điều có thể nói chắc:

  • Ở k nhỏ, thứ người dùng thật sự trải nghiệm, RLVR cải thiện rõ.
  • Ở k rất lớn, với ngân sách RL như các paper trên, nhiều kết quả cho thấy base model không thua.
  • Kết luận phụ thuộc thước đo (pass@k có kiểm lập luận hay không), độ dài train RL, và model xuất phát.

Với người dùng, “chọn đúng con đường suy luận ngay lần đầu” đã là điều làm reasoning model hữu ích. Với người train model, câu hỏi này quyết định nên đầu tư vào RL dài hơn hay vào distillation và dữ liệu pretraining.


08Thinking tokens có giá

Thinking tokens là token output bình thường

Với model, không có gì đặc biệt ở token nằm giữa <think> và </think>. Chúng được sinh ra bằng cùng một vòng decode, từng token một. Ba hệ quả:

  • Tính tiền như output. Tài liệu của cả ba nhà cung cấp lớn đều ghi thinking/reasoning tokens được tính tiền như output tokens, kể cả khi API không trả chúng về (mục “Các núm điều khiển” bên dưới).
  • Chiếm KV cache. Mỗi thinking token ghi K, V của nó vào cache như mọi token khác, và mọi token sau phải đọc lại chúng (bài 13, mục 3).
  • Chạy ở pha decode, pha memory-bound (bài 13, mục 2). Một prompt 1.000 token xử lý trong một lượt prefill; 1.000 thinking tokens cần 1.000 lượt decode.

Tính bằng roofline

Áp mô hình roofline của bài 13 vào hai cấu hình đã tính ở đó. Bảng dưới là phép tính lý thuyết, không phải số đo: thời gian là cận dưới khi chỉ tính byte phải đọc (weights cộng KV cache) chia băng thông, ở batch 1, với prompt 200 token.

Thinking tokens Qwen2.5-1.5B trên RTX 2080 Ti: thời gian decode tối thiểu KV cache cuối chuỗi Llama 3.1 8B trên GPU 80 GB: KV cache mỗi sequence Số sequence đồng thời tối đa
1.000 ≈ 5 giây ≈ 0,03 GB ≈ 0,16 GB ≈ 355
10.000 ≈ 53 giây ≈ 0,29 GB ≈ 1,3 GB ≈ 41
32.000 ≈ 185 giây ≈ 0,92 GB ≈ 4,2 GB ≈ 13

Cột Qwen2.5-1.5B dùng số của bài 13: 3,09 GB weights, 28 KiB KV cache mỗi token, băng thông 616 GB/s; mỗi bước decode tốn khoảng 5 ms. Cột Llama 3.1 8B dùng 128 KiB mỗi token và ngân sách KV cache khoảng 56 GB của một GPU 80 GB, cũng từ bài 13; con số 13 sequence ở 32k token khớp với bảng ở đó.

Hai điều đọc được:

  • Latency tăng tuyến tính theo số thinking tokens: với model 1,5B, 32 nghìn token suy nghĩ là ba phút chờ ở mức lý thuyết tốt nhất. Reasoning model “chậm” vì nhiều token hơn, không phải vì mỗi token chậm hơn.
  • Throughput của nhà cung cấp giảm theo độ dài suy nghĩ: một sequence nghĩ 32 nghìn token chiếm KV cache bằng 32 sequence nghĩ 1 nghìn token, mà KV cache quyết định batch. Đây cũng là chỗ MoE và state cố định của bài 14 làm reasoning model rẻ hơn, và chỗ speculative decoding có thể giảm latency.

Các núm điều khiển ngân sách

Vì nghĩ lâu tốn tiền và thời gian, mọi API đều có cách điều khiển độ dài suy nghĩ. Bảng dưới lấy từ tài liệu chính thức ngày 14/10/2026:

Nhà cung cấp Tham số Giá trị Chuỗi suy nghĩ trả về
Anthropic (Claude 4.5 trở về trước) thinking: {type: "enabled", budget_tokens: N} N ≥ 1.024 và nhỏ hơn max_tokens; docs gọi đây là “mục tiêu, không phải trần cứng” bản tóm tắt (với các model Claude 4); tính tiền theo toàn bộ thinking tokens
Anthropic (Claude 4.6 trở đi) thinking: {type: "adaptive"} kèm effort low, medium, high, xhigh, max tuỳ model; budget_tokens bị deprecated ở 4.6 và bị từ chối (lỗi 400) từ 4.7 display: summarized, omitted (mặc định trên các model Claude 5.x) hoặc updates (beta, chỉ trả các cập nhật tiến độ giữa các lần gọi tool); không có chế độ nào trả chuỗi thô
Google Gemini 2.5 thinkingBudget (tài liệu generateContent, nay mang nhãn Legacy) 2.5 Pro: 128–32.768, không tắt được; 2.5 Flash: 0–24.576; -1 là tự động chỉ trả câu trả lời, trừ khi bật includeThoughts để nhận tóm tắt
Google Gemini 3 trở đi thinkingLevel (bắt buộc từ Gemini 4) minimal, low, medium, high tuỳ model; minimal “không đảm bảo tắt thinking” như trên
OpenAI reasoning.effort (Responses API), reasoning_effort (Chat Completions) tuỳ model, trong none, minimal, low, medium, high, xhigh, max; nhiều model mặc định medium không trả reasoning tokens; có tham số summary để nhận tóm tắt
gpt-oss (open-weight) dòng Reasoning: low/medium/high trong system prompt mặc định medium trả đầy đủ ở kênh analysis; model card khuyên không hiện thẳng cho người dùng
Qwen3 (open-weight) enable_thinking trong chat template; /think, /no_think trong prompt mặc định True; /think, /no_think chỉ có tác dụng khi enable_thinking=True, model theo chỉ thị gần nhất trả đầy đủ trong <think>…</think>

Ba điều đáng để ý:

  • Ngân sách là mục tiêu, không phải hợp đồng. Google ghi model “có thể vượt hoặc dùng ít hơn” ngân sách tuỳ prompt. Hướng mới của cả Anthropic và Google là chuyển sang mức effort hay thinkingLevel và để model tự quyết nghĩ bao nhiêu.
  • Trả tiền cho thứ không thấy. Anthropic ghi người dùng “bị tính tiền cho toàn bộ thinking tokens mà request gốc sinh ra, không phải số token của bản tóm tắt”; Google và OpenAI ghi tương tự.
  • Phải chừa chỗ. max_output_tokens của OpenAI tính cả reasoning tokens, và docs khuyên chừa ít nhất 25.000 token khi bắt đầu thử; giới hạn thấp quá thì model có thể hết token khi chưa kịp trả lời.

Budget forcing: điều khiển độ dài từ phía decoding

Với model open-weight, ngân sách có thể ép ngay trong vòng decode. s1 (Muennighoff et al. 2025) fine-tune Qwen2.5-32B-Instruct trên chỉ 1.000 câu hỏi khó có chuỗi suy nghĩ mẫu, tốn 26 phút trên 16 GPU H100 (mục 1), rồi điều khiển độ dài bằng budget forcing (mục 3.1):

  • Cắt: hết ngân sách thì chèn token kết thúc suy nghĩ, buộc model chuyển sang trả lời.
  • Kéo dài: khi model định kết thúc suy nghĩ, chặn token đó lại và chèn chữ “Wait”, để model nghĩ tiếp và thường tự kiểm tra lại.

Trên AIME 2024, budget forcing nâng s1-32B từ 50,0% lên 56,7% (Bảng 1). Lợi ích phẳng dần khi ép kéo dài khoảng sáu lần (mục 4.2, Hình 4), và trong các cách kéo dài đã thử, chữ “Wait” cho kết quả tốt nhất (Bảng 4). Qwen3 làm cách “cắt” tương tự cho ngân sách suy nghĩ: khi chạm ngưỡng, hệ thống dừng suy nghĩ và chèn một câu chỉ dẫn kiểu “do thời gian có hạn, tôi phải đưa ra lời giải ngay”, rồi </think> (báo cáo kỹ thuật Qwen3, mục 4.3). Theo báo cáo, khả năng trả lời từ suy nghĩ dở dang này không được train riêng mà tự xuất hiện.

Độ dài suy nghĩ vì vậy điều khiển được từ bên ngoài, ở mức decoding, không cần đổi weights.


09Khi nào nghĩ lâu hơn thì đáng

Có lợi: nhiều bước, có đáp án kiểm được

Bằng chứng ở các mục trước chỉ về cùng một hướng. Nghĩ lâu hơn có lợi rõ nhất khi bài toán:

  • cần nhiều bước tính tuần tự (toán, code, logic, lập kế hoạch), như lập luận ở mục 2;
  • có tiêu chí đúng/sai rõ, để train bằng RLVR và để sinh nhiều lần rồi chọn bằng verifier;
  • ở độ khó vừa với model: trả lời ngay hay sai, nhưng không phải gần như không bao giờ đúng (mục 4).

Ít hoặc không lợi: tri thức và câu dễ

  • Tra cứu kiến thức: theo Sprague et al., các nhóm tác vụ ngoài toán và logic gần như không đổi khi dùng CoT (từ 56,1 lên 56,8). Nghĩ thêm không làm model biết thêm một sự kiện nó chưa từng thấy; đó là việc của retrieval.
  • Câu đơn giản: trả lời ngay đã đúng, mọi thinking token là chi phí thuần.
  • Tác vụ không có tiêu chí đúng/sai rõ (viết, tóm tắt, hội thoại): không có verifier để train, không có đáp án để bầu; lợi ích nếu có thì khó đo.

Có hại: overthinking và inverse scaling

Nghĩ lâu hơn không chỉ có thể vô ích mà còn có thể làm kết quả tệ đi.

Overthinking. Chen et al. 2024 (Do NOT Think That Much for 2+3=?) hỏi các model “2 cộng 3 bằng mấy”. QwQ-32B-Preview dùng 901 token, DeepSeek-R1 dùng 987 (số ở bản v2 của paper), trong khi GPT-4o dùng 7 token và Qwen2.5-72B dùng 12 (Hình 1a). Trung bình các model kiểu o1 dùng nhiều hơn 1.953% số token so với model thường (mục 1). Câu trả lời của QwQ cho câu này chứa 13 lời giải (Hình 2), và đáp án đúng xuất hiện ngay ở token thứ 39: chỉ 39/901 ≈ 4,3% số token là cần thiết (mục 2.2).

Inverse scaling. Gema et al. 2025 dựng các tác vụ mà độ chính xác giảm khi tăng độ dài suy luận, trên các model Claude, dòng o của OpenAI, Qwen3, QwQ và DeepSeek R1. Bốn nhóm tác vụ (abstract): đếm đơn giản có thông tin gây nhiễu, hồi quy có đặc trưng giả, suy diễn cần theo dõi nhiều ràng buộc, và các kịch bản rủi ro AI. Kiểu thất bại khác nhau tuỳ họ model: bị thông tin nhiễu kéo đi, bám vào cách đóng khung quen thuộc của đề, hay chuyển sang tương quan giả; trong kịch bản rủi ro, Claude Sonnet 4 bày tỏ ý muốn tự bảo tồn nhiều hơn khi nghĩ lâu hơn.

Rút gọn chuỗi suy nghĩ. Kimi k1.5 (Kimi Team 2025) train model nghĩ ngắn lại, gọi là long2short, bằng bốn cách: trộn weights model nghĩ dài với model nghĩ ngắn, rejection sampling giữ lời giải đúng ngắn nhất, DPO ưu tiên câu ngắn, và RL có phạt độ dài (mục 2.4). Model long2short RL của họ đạt 60,8 điểm AIME 2024 với trung bình chỉ 3.272 token (mục 3.4). Mức effort thấp và chế độ non-thinking của API là cùng ý tưởng, ở phía người dùng.

Bảng quyết định

Loại tác vụ Nên làm Vì sao
Toán, bài đố nhiều bước có đáp án số bật thinking, effort trung bình tới cao; nếu cần độ tin cậy thì sinh nhiều lần và bầu đa số nhiều bước tính, đáp án so được (mục 2, 3)
Code có test bật thinking; sinh nhiều lần và chọn bằng test test là verifier gần như hoàn hảo; coverage tăng theo số mẫu (mục 3)
Lập kế hoạch, ràng buộc nhiều bước bật thinking, theo dõi độ dài có lợi, nhưng là vùng inverse scaling với ràng buộc phức tạp (mục 9)
Tra cứu kiến thức, QA sự kiện tắt thinking hoặc effort thấp; dùng retrieval CoT gần như không giúp tác vụ tri thức (mục 2)
Câu đơn giản, phân loại, trích xuất tắt thinking chỉ tốn token, có rủi ro overthinking
Viết, tóm tắt, hội thoại effort thấp; thử cả hai trên dữ liệu của mình không có tiêu chí đúng/sai, lợi ích khó đo
Ứng dụng nhạy latency tắt thinking hoặc effort thấp nhất latency tăng tuyến tính theo thinking tokens (mục 8)

Bảng này là điểm xuất phát, không thay được việc đo trên dữ liệu thật: thinking tokens luôn có giá, lợi ích thì không phải lúc nào cũng có.


10Chuỗi suy nghĩ có trung thực không

Chuỗi suy nghĩ trông như lời model tự giải thích. Câu hỏi là nó có phản ánh lý do thật khiến model ra đáp án không. Mục này ngắn, phần sâu về interpretability để bài 17.

  • CoT có thể không trung thực. Turpin et al. 2023 thêm vào prompt các đặc trưng gây thiên lệch, ví dụ một câu “Tôi nghĩ đáp án là…”. Với GPT-3.5 (text-davinci-003) ở zero-shot CoT, gợi ý này làm độ chính xác giảm tới 36,3 điểm phần trăm trên các tác vụ BIG-Bench Hard, tính trên các câu mà đáp án đúng ngược với gợi ý (mục 3.2, Bảng 8), nhưng chuỗi lập luận thường không nhắc tới gợi ý. Model đưa ra một lời giải nghe hợp lý cho đáp án mà gợi ý đã đẩy nó tới.
  • Reasoning model cũng vậy. Chen et al. 2025 (Anthropic) cài sáu loại gợi ý vào câu hỏi và kiểm tra xem model có thừa nhận đã dùng gợi ý không. Claude 3.7 Sonnet nhắc tới gợi ý trong 25% trường hợp, DeepSeek R1 trong 39% (mục 3, Hình 1). RL dựa trên kết quả ban đầu làm tỉ lệ này tăng, rồi chững lại ở khoảng 28% trên MMLU và 20% trên GPQA (mục 4, Hình 5). Trong các môi trường có sẵn cách hack reward, model dùng cách hack ở hơn 99% ví dụ nhưng nhắc tới nó trong ít hơn 2%, ở 5 trên 6 môi trường (mục 5.2, Hình 7).
  • Theo dõi CoT vẫn hữu ích, nhưng mong manh. Baker et al. 2025 dùng GPT-4o để theo dõi reasoning model trong các tác vụ code ở mục 6. Monitor đọc cả CoT lẫn hành động bắt được 95% các vụ hack reward, so với 60% khi chỉ đọc hành động (Bảng 1). Nhưng trong thí nghiệm ở mục 3, với một agent không phải model frontier và một monitor chỉ đọc CoT, khi đưa monitor vào reward để phạt “ý định hack”, model học cách giấu ý định: vẫn hack, nhưng CoT trông vô hại, và tỉ lệ bắt được của monitor tụt gần về 0 (mục 3.2). Nhóm tác giả khuyên chấp nhận một “monitorability tax”: đừng đặt áp lực tối ưu trực tiếp lên CoT.

Đây cũng là lý do OpenAI nêu khi ẩn chuỗi suy nghĩ của o1: để CoT hữu ích cho việc theo dõi, họ không train chính sách nội dung lên CoT, nên không muốn hiện thẳng cho người dùng. Model card của gpt-oss cũng không đặt áp lực tối ưu lên CoT và khuyên không hiện thẳng CoT cho người dùng khi chưa lọc hoặc tóm tắt.

Cách đọc hợp lý: CoT là dữ liệu chẩn đoán có ích, không phải lời khai đáng tin. Nó giúp phát hiện hành vi sai khi model chưa có lý do để giấu, và mất tác dụng khi bị tối ưu.


11Những chỗ dễ hiểu sai

Chín hiểu lầm dưới đây khớp với hàng trên và hàng dưới của Hình 1.

Hiểu lầm Thực tế
“Model dùng đúng ngân sách được cấp” Ngân sách là mục tiêu, không phải hợp đồng: Anthropic gọi budget_tokens là “mục tiêu, không phải trần cứng”, Google ghi model có thể vượt hoặc dùng ít hơn; hướng mới là mức effort để model tự quyết (mục 8)
“Reasoning model là một kiến trúc khác” Vẫn là Transformer decoder như mọi LLM, khác ở cách train (RLVR, distillation) và cách dùng lúc trả lời. Qwen3 bật và tắt thinking trong cùng một bộ weights (mục 2, 5)
“Aha moment là khả năng tự dưng xuất hiện từ số không” Hành vi tự kiểm tra đã có trong hầu hết base model, kể cả DeepSeek-V3-Base; RL khuếch đại nó, và nó không tương quan thuận với độ chính xác (mục 7)
“Nghĩ càng lâu càng đúng” Lợi ích phẳng dần (s1 phẳng ở khoảng 6 lần), có overthinking ở câu dễ và inverse scaling ở một số tác vụ, nơi nghĩ lâu hơn làm kết quả tệ đi (mục 8, 9)
“Chuỗi suy nghĩ cho thấy model thật sự nghĩ gì” Reasoning model chỉ nhắc tới gợi ý đã dùng trong 25–39% trường hợp; khi bị tối ưu, CoT học cách giấu hành vi (mục 10)
“Reward kiểm chứng được thì không thể hack” Không có reward model xấp xỉ để hack, nhưng verifier là chương trình và có thể có lỗ hổng: exit(0), bỏ qua test, sửa file test (mục 6)
“RL dạy model kỹ năng hoàn toàn mới” Đang tranh luận: ở k lớn, base model đuổi kịp model RL trong nhiều thí nghiệm; ProRL báo điều ngược lại khi train RL rất lâu (mục 7)
“Thinking tokens miễn phí hoặc rẻ hơn output” Tính tiền như output kể cả khi bị ẩn, chiếm KV cache và chạy ở pha decode memory-bound; latency tăng tuyến tính theo số token (mục 8)
“Self-consistency dùng được cho mọi tác vụ” Cần đáp án trích ra và so được; khi một đáp án sai phổ biến hơn đáp án đúng, càng nhiều mẫu càng chắc chắn sai (Code 2, mục 3)

Tổng kết: chất lượng mua được, nhưng có điều kiện

Bài 10 mua chất lượng bằng compute lúc train. Bài này cho thấy compute lúc trả lời cũng mua được chất lượng, trong một vùng nhất định.

  • Chain-of-thought là cách model mượn thêm forward pass, nên giúp ở bài nhiều bước (toán, code, logic), gần như không giúp tác vụ tri thức.
  • Ba cách tiêu compute lúc trả lời: song song, tuần tự và tìm kiếm. Coverage tăng đều theo số mẫu, nhưng chọn đúng mẫu mới là bài toán thật; bầu đa số bão hoà và có thể làm tệ đi.
  • Test-time compute không thay được tham số 1-1: model nhỏ nghĩ thêm thắng model lớn hơn 14 lần ở câu vừa sức, không thắng ở câu khó nhất.
  • Reasoning model được train bằng RL với reward kiểm chứng được. R1-Zero tự nghĩ dài hơn chỉ nhờ reward đúng/sai và format; R1 thêm cold-start, rejection sampling và distillation.
  • GRPO lấy trung bình nhóm làm baseline, nên nhóm đồng nhất không cho gradient và độ khó dữ liệu quyết định tín hiệu. Verifier là trần của RLVR: verifier hở thì bị khai thác.
  • RL dạy cái mới hay chỉ làm sắc cái có sẵn vẫn đang tranh luận.
  • Thinking tokens là token output bình thường: tính tiền như output, chiếm KV cache, chạy ở pha decode; ngân sách trong API là mục tiêu, không phải hợp đồng.
  • Nghĩ lâu hơn có lợi ở bài nhiều bước có đáp án kiểm được, ít lợi ở tra cứu kiến thức, có hại khi overthinking hay inverse scaling. CoT là dữ liệu chẩn đoán, không phải lời khai.

Bài này đã gặp nhiều con số benchmark: AIME, MATH, GSM8K, pass@k. Bài 16 sẽ hỏi câu tiếp theo: vì sao LLM sai, và các con số đó đo đúng tới đâu, từ hallucination tới contamination và giới hạn của chính các benchmark.


Tài liệu tham khảo

Paper: chain-of-thought và test-time compute

Paper: reasoning model và RL

Paper: overthinking và faithfulness

Blog, tài liệu API và model card (lấy ngày 14/10/2026)

Trương Đức Dũng

AI Engineer - Hà Nội

Tập trung vào LLM, RAG và các hệ thống AI cần chạy ổn định trong môi trường vận hành thật.