$ cat ./blog/gen-ai/llm-fundamentals/attention-self-attention.md
seriesCách hoạt động của LLMbài 7

Attention và Self-Attention: cơ chế khiến một token hiểu được ngữ cảnh

Bài thứ bảy trong series Cách hoạt động của LLM: vì sao một vector embedding chưa đủ để biểu diễn token trong câu, Query - Key - Value thật ra là gì, công thức scaled dot-product làm gì ở từng bước, causal mask chặn cái gì, multi-head thêm được gì, và vì sao context dài lại đắt đến thế.

~/diagrams/attention-self-attention/attention-pipeline.htmltương tác
mở riêng ↗
HìnhHàng trên là bốn bước thật sự xảy ra trong một attention head: hidden state được chiếu thành Q, K và V; Q gặp K để ra điểm tương thích; điểm được chia thang rồi chuẩn hoá thành trọng số; trọng số trộn V lại thành một vector mới. Hàng dưới là những chỗ trực giác hay đi sai - vector tĩnh không đủ, Q/K/V không được lưu sẵn, mask cộng vào trước softmax chứ không phải sau, và output là một hỗn hợp chứ không phải một token được chọn.
bốn bước của attentiontham số và maskchỗ trực giác hay đi sai

Bài thứ sáu dừng lại ở chỗ một chuỗi token đã trở thành một dãy vector:

Text
  ->  Tokenization
  ->  [token_1, token_2, ..., token_n]
  ->  Embedding
  ->  [x_1, x_2, ..., x_n]

Nhưng có một vấn đề mà bài đó cố tình để lại: một vector embedding ban đầu chưa đủ để biểu diễn ý nghĩa của token trong ngữ cảnh cụ thể.

Từ bank trong river bank và trong bank account nhận cùng một vector ở cửa vào model, nếu tokenizer sinh ra cùng một token. Nhưng nghĩa của chúng khác hẳn nhau. Model cần một cơ chế để representation của mỗi token thay đổi được theo những token đứng quanh nó.

Đó là việc của Self-Attention.

Nếu embedding trả lời câu hỏi “token này là gì”, thì self-attention trả lời câu hỏi khó hơn: “token này có nghĩa gì trong câu hiện tại, và tôi nên lấy thông tin từ token nào khác để hiểu nó”.

Cả bài đi tới đúng một công thức:

Attention(Q, K, V) = softmax( Q Kᵀ / căn(d_k) ) V

Mục tiêu không phải là thuộc công thức. Mục tiêu là sau khi đọc xong, nhìn vào từng thành phần và nói được nó đang làm gì, vì sao cần nó, và nó nằm ở đâu trong một LLM thật. Sơ đồ ở đầu bài (Hình 1) là toàn bộ chặng đó gói trong một hình.


01Từ embedding đến ngữ cảnh: vì sao cần attention

Lấy một câu quen thuộc trong mọi bài giảng về Transformer:

The animal didn't cross the street because it was tired.

Tokenizer cắt câu thành token, embedding layer biến mỗi token thành một vector. Ở tầng đầu tiên, trước khi các token trao đổi gì với nhau, representation của it chưa chứa thông tin nào nói rằng it có liên quan đến animal.

Việc model cần làm là tạo ra một representation mới cho it bằng cách kết hợp có chọn lọc thông tin từ những token khác trong câu.

~/diagrams/attention-self-attention/why-attention.htmltương tác
mở riêng ↗
HìnhHàng trên là đường đi thật: token hỏi những token nào liên quan, tính mức liên quan, gán trọng số, rồi tổng hợp lại thành một vector mới. Hàng dưới là hai cách ai cũng nghĩ tới đầu tiên và lý do cả hai đều hỏng - lấy trung bình thì coi mọi token quan trọng như nhau, còn chỉ nhìn token gần thì bỏ mất những quan hệ ở xa.
đường đi tới ngữ cảnhtrọng số học đượchai cách ngây thơ

Cách đơn giản nhất là lấy trung bình toàn bộ embedding trong câu:

(x_1 + x_2 + ... + x_n) / n

Nhưng như vậy mọi token đều quan trọng như nhau. Trong ngôn ngữ, điều đó gần như không bao giờ đúng.

Cách thứ hai là chỉ nhìn vài token gần nhất. Nhưng quan hệ ngữ nghĩa và cú pháp không phụ thuộc vào khoảng cách. Chủ ngữ có thể cách động từ rất nhiều token. Một đại từ có thể trỏ về danh từ ở đầu đoạn. Một dấu ngoặc mở trong code có thể đóng ở rất xa.

Attention giải quyết chuyện này bằng cách để model học một trọng số riêng cho từng quan hệ giữa hai token:

Embedding ban đầu
  ->  token chưa biết token nào quan trọng với mình
  ->  tính mức độ liên quan với từng token khác
  ->  gán attention weight
  ->  tổng hợp thông tin theo các weight đó
  ->  contextualized representation

Ý tưởng “chọn mềm” này xuất hiện trước Transformer, trong neural machine translation. Bahdanau, Cho và Bengio đề xuất để decoder không phải nén cả câu nguồn vào một vector cố định, mà có thể soft-search những phần của câu nguồn liên quan đến từ đang được sinh ra. Transformer sau đó đưa attention lên thành cơ chế trung tâm và bỏ hẳn recurrence khỏi đường xử lý chính.

Một điểm cần phân biệt ngay từ đầu. Ở layer đầu tiên, input là token embedding cộng thông tin vị trí. Ở các layer sâu hơn, input của self-attention đã là hidden state có ngữ cảnh từ những layer trước. Vì vậy đừng hiểu rằng mọi self-attention layer đều nhận “embedding chưa có ngữ cảnh”. Đúng hơn: mỗi layer tiếp tục tinh chỉnh representation bằng cách cho các vị trí trao đổi thêm thông tin.


02Attention thực chất là một phép tra cứu mềm

Cách dễ hiểu nhất để nhìn attention là coi nó như một hệ thống tra cứu dựa trên Query - Key - Value.

Tưởng tượng một database:

Key            Value
-------------------------------------
"animal"       thông tin về animal
"street"       thông tin về street
"tired"        thông tin về tired

Có một Query, hệ thống tìm Key nào khớp nhất rồi trả về Value tương ứng.

Attention làm gần như vậy, chỉ khác một chỗ quan trọng: thay vì chọn đúng một Key, nó cho phép Query khớp với nhiều Key ở những mức độ khác nhau.

~/diagrams/attention-self-attention/qkv-soft-lookup.htmltương tác
mở riêng ↗
HìnhBa làn tương ứng ba câu hỏi: Query là tôi đang tìm gì, Key là tôi có hợp với thứ đang được tìm không, Value là nếu được chú ý tới thì tôi gửi thông tin gì. Điểm quyết định nằm ở chỗ Key và Value tách làm hai - một không gian để so khớp, một không gian để truyền nội dung, và model học cả hai một cách độc lập.
đường đi của một querykhớp nhiều key một lúcnội dung được truyền

Ba khái niệm gói gọn trong ba câu hỏi:

  • Query (Q): tôi đang tìm loại thông tin gì?
  • Key (K): tôi có phù hợp với thứ Query đang tìm không?
  • Value (V): nếu tôi được chú ý tới, thông tin nào của tôi sẽ đi sang output?

Điểm quan trọng nhất trong cả phần này: Key và Value có vai trò khác nhau.

Key dùng để quyết định mức độ liên quan. Value là thông tin thực sự được tổng hợp. Đây là lý do attention không đơn giản là “tính similarity giữa các embedding rồi cộng embedding lại”. Model có thể học hai không gian tách biệt - một không gian để khớp và một không gian để truyền thông tin.

Về mặt toán, với một query q, các key k_i và value v_i, output có dạng:

out(q) = tổng theo i của ( alpha_i * v_i )

trong đó alpha_i là attention weight của value thứ i. Token rất liên quan thì alpha_i lớn. Gần như không liên quan thì alpha_i gần 0 - nhưng không bao giờ bị loại hẳn khỏi phép tính.

Đây là lý do có thể gọi attention là một dạng content-addressable retrieval: thay vì truy cập thông tin bằng địa chỉ cố định, model truy cập dựa trên nội dung mà Query và Key biểu diễn.


03Attention, Self-Attention và Cross-Attention

“Attention” là tên của cơ chế tổng quát. Self-Attention và Cross-Attention mô tả nguồn mà Q, K, V được tạo ra - không phải hai cơ chế khác nhau về bản chất.

~/diagrams/attention-self-attention/self-vs-cross.htmltương tác
mở riêng ↗
HìnhHàng trên là self-attention: chỉ có một sequence, và cả Q, K lẫn V đều được chiếu ra từ chính nó. Hai hàng dưới là cross-attention: Query đến từ một stream, Key và Value đến từ stream khác, nên decoder có thể đọc thông tin của encoder. Chữ self nói về nguồn, không nói về phạm vi được nhìn - cái giới hạn phạm vi là mask.
một nguồn duy nhấthai nguồn tách biệthệ quả của từng cách

Với Self-Attention, cùng một input X được chiếu sang ba không gian:

Q = X W_Q
K = X W_K
V = X W_V

Mỗi position trong sequence dùng Query của mình để tìm thông tin từ các Key của chính sequence đó.

Sequence X
   |----------> Q
   |----------> K
   +----------> V

Với Cross-Attention, hai nguồn tách nhau:

Stream A  ----->  Q

Stream B  ----->  K
          +---->  V

Trong Transformer encoder-decoder cổ điển, decoder tạo Query, còn Key và Value đến từ output của encoder. Nhờ đó decoder lấy được thông tin từ câu nguồn khi sinh câu đích.

Trong các decoder-only language model kiểu GPT, cơ chế cốt lõi là causal self-attention: Q, K, V cùng đến từ chuỗi token đang xử lý, nhưng mỗi vị trí bị cấm nhìn những token nằm ở tương lai.

Cơ chế Q đến từ K, V đến từ Token được phép nhìn
Encoder self-attention encoder states encoder states thường là toàn bộ sequence
Decoder causal self-attention decoder states decoder states chỉ hiện tại và quá khứ
Cross-attention decoder / query stream encoder / memory stream toàn bộ nguồn được cung cấp

Bài này tập trung vào self-attention, vì đó là cơ chế nằm trực tiếp trong lõi của các LLM decoder-only hiện đại.


04Q, K, V đến từ đâu

Đây là điểm dễ gây nhầm lẫn nhất trong cả bài.

Q, K và V không phải ba loại embedding được lưu sẵn trong vocabulary. Chúng được tính từ hidden representation bằng những phép chiếu tuyến tính có tham số học được.

Giả sử sequence có n token và mỗi hidden vector có d_model chiều. Gom chúng thành một ma trận X cỡ n × d_model. Model giữ ba ma trận trọng số:

W_Q  cỡ  d_model × d_k
W_K  cỡ  d_model × d_k
W_V  cỡ  d_model × d_v

Sau đó, với từng token x_i:

q_i = x_i W_Q
k_i = x_i W_K
v_i = x_i W_V
~/diagrams/attention-self-attention/qkv-projection.htmltương tác
mở riêng ↗
HìnhCùng một hidden state đi qua ba ma trận trọng số khác nhau và ra ba vector khác nhau. Ba làn đọc theo ba vai trò - Query là phía đặt câu hỏi, Key là phía được so khớp, Value là phía truyền nội dung. Điều dễ quên là ba ma trận này là tham số được học end-to-end, nên vai trò trong sơ đồ là cách diễn giải chứ không phải ràng buộc do kiến trúc áp đặt.
một nguồn, ba đíchma trận học đượcvai trò của từng vector

Một trực giác hữu ích:

x_i
 |
 |-- W_Q -> q_i : token i muốn tìm gì?
 |
 |-- W_K -> k_i : token i được tìm thấy theo tiêu chí nào?
 |
 +-- W_V -> v_i : nếu token i được chọn, nó gửi thông tin gì?

W_Q, W_K và W_V là learnable parameters. Trong training, gradient descent điều chỉnh chúng sao cho những phép khớp Q-K và những Value được truyền đi trở nên hữu ích cho objective của model.

Không có người lập trình nào quy định rằng:

Q = chủ ngữ
K = động từ
V = ngữ nghĩa

Những representation này được học end-to-end. Đây cũng là lý do không nên diễn giải một attention head quá cứng nhắc theo kiểu “head này luôn làm coreference, head kia luôn làm syntax”. Một số head có thể biểu hiện pattern dễ đọc trên một số input, nhưng vai trò của chúng là emergent và có thể phân tán giữa nhiều head và nhiều layer.


05Scaled Dot-Product Attention, từng bước một

Đây là công thức trung tâm:

Attention(Q, K, V) = softmax( Q Kᵀ / căn(d_k) ) V

Thay vì nhìn cả công thức cùng lúc, hãy tách nó thành một pipeline bốn bước.

~/diagrams/attention-self-attention/scaled-dot-product.htmltương tác
mở riêng ↗
HìnhHàng trên là bốn phép tính nối nhau: tích vô hướng ra điểm tương thích, chia căn d_k để giữ thang, cộng mask để loại ô bị cấm, softmax để thành trọng số, rồi nhân V để lấy thông tin. Hàng dưới trả lời câu hỏi vì sao - chia căn d_k vì phương sai của tích vô hướng tăng theo d_k, và softmax vì mỗi hàng cần trở thành một phân phối cộng lại bằng 1.
bốn bước tính toánchỗ mask can thiệplý do của từng bước

5.1 Bước 1 - tính similarity bằng Q Kᵀ

Nếu Q cỡ n × d_k và K cỡ n × d_k, thì Q Kᵀ cỡ n × n. Phần tử ở hàng i, cột j là:

s_ij = q_i · k_j

Nó trả lời đúng một câu hỏi: với Query của token i, Key của token j phù hợp đến mức nào? Tích vô hướng càng lớn thì hai vector càng thẳng hàng theo phép đo mà model đã học.

5.2 Bước 2 - chia cho căn(d_k)

Transformer không dùng q · k trần, mà dùng:

q_i · k_j / căn(d_k)

Tại sao? Giả sử các thành phần của q và k độc lập, trung bình xấp xỉ 0 và phương sai xấp xỉ 1. Khi đó:

q · k = tổng theo r từ 1 đến d_k của ( q_r * k_r )

có phương sai tăng theo d_k. Khi d_k lớn, tích vô hướng dễ có độ lớn rất cao. Đưa những score quá lớn vào softmax làm phân phối trở nên cực đoan - gần như toàn bộ khối lượng dồn vào một ô - và khi đó gradient ở phần lớn vị trí gần bằng 0. Chia cho căn(d_k) giữ thang giá trị ổn định hơn.

Đây không phải một hằng số tuỳ ý. Nó gắn trực tiếp với số chiều của Query và Key.

5.3 Bước 3 - softmax biến score thành weight

Với token i:

alpha_ij = exp(s_ij / căn(d_k)) / tổng theo t của exp(s_it / căn(d_k))

Khi đó alpha_ij không âm và tổng theo j bằng đúng 1. Nói cách khác, với mỗi Query, softmax biến một hàng score thành một phân phối trọng số trên các Key.

5.4 Bước 4 - lấy weighted sum của Value

Output tại position i:

o_i = tổng theo j của ( alpha_ij * v_j )

Đây chính là lúc thông tin thực sự được truyền giữa các token.

5.5 Một ví dụ số nhỏ

Giả sử một Query có tích vô hướng với ba Key là [1.2, 2.4, 0.3], và d_k = 4 nên căn(d_k) = 2. Sau khi chia:

[0.6, 1.2, 0.15]

Softmax cho xấp xỉ:

[0.289, 0.527, 0.184]

Nếu ba Value là v_1 = [1, 0], v_2 = [0, 2] và v_3 = [1, 1], thì:

o = 0.289 * v_1 + 0.527 * v_2 + 0.184 * v_3
o ≈ [0.473, 1.238]

Chú ý kết quả: nó không trùng với bất kỳ Value nào. Output không phải bản sao của một token được chọn, mà là một hỗn hợp có trọng số.

Toàn bộ trực giác của công thức có thể rút thành bốn câu:

Q Kᵀ           ->  ai liên quan tới ai?
chia căn d_k   ->  giữ thang giá trị ổn định
softmax        ->  liên quan bao nhiêu?
nhân V         ->  lấy thông tin gì từ họ?

Và một phiên bản pseudo-code rất gần với cài đặt thật:

scores = Q @ K.transpose(-2, -1)
scores = scores / sqrt(d_k)
scores = scores + mask      # nếu có
weights = softmax(scores, dim=-1)
output = weights @ V

Đây gần như đúng logic của scaled_dot_product_attention trong các framework hiện đại, dù bản production có thể dùng kernel fused hoặc thuật toán IO-aware để chạy nhanh hơn.


06Attention matrix: mỗi token đang nhìn token nào

Với sequence dài n token, ma trận

A = softmax( Q Kᵀ / căn(d_k) )

có cỡ n × n. Mỗi hàng là một Query token. Mỗi cột là một Key token.

~/diagrams/attention-self-attention/attention-matrix.htmltương tác
mở riêng ↗
HìnhĐọc theo hàng cho biết một Query đang phân bổ chú ý ra sao, đọc theo cột cho biết một Key đang được ai nhìn. Heatmap dựng từ ma trận này rất hữu ích để debug và quan sát cơ chế cục bộ của một head. Nhưng nhánh bên phải là chỗ cần dừng lại - weight cao không đồng nghĩa với nguyên nhân, vì output cuối còn đi qua residual, nhiều head, MLP và hàng trăm layer.
đọc theo hàng và cộtdùng để debuggiới hạn của cách đọc

Một ví dụ minh hoạ giả định:

Query ↓ / Key → The animal was tired
The 0.55 0.25 0.10 0.10
animal 0.15 0.45 0.15 0.25
was 0.08 0.27 0.40 0.25
tired 0.05 0.45 0.15 0.35

Nếu hàng tired đặt trọng số lớn vào animal, hidden representation mới của tired sẽ lấy một phần đáng kể thông tin từ Value của animal.

                     Keys
             The  animal  was  tired
Query  The    ###    ##     .     .
       animal  .    ###     .    ##
       was     .     ##    ###   ##
       tired   .    ###     .    ##

Cách hình dung này thường được gọi là attention heatmap, và nó là một cửa sổ rất tiện để quan sát những interaction mà một head đang tạo ra.

Nhưng cần rất cẩn thận với câu sau:

“Token có attention weight cao nhất chính là lý do model đưa ra prediction.”

Điều đó không nhất thiết đúng.

Attention weight cho biết mức độ Value được sử dụng bên trong chính phép attention đó. Output cuối cùng của LLM còn đi qua residual connection, nhiều attention head, MLP/FFN, normalization và hàng chục tới hàng trăm layer. Nghiên cứu về interpretability đã cho thấy có trường hợp những attention distribution rất khác nhau vẫn dẫn tới prediction tương tự, và tương quan giữa attention weight với các thước đo feature importance khác không phải lúc nào cũng cao.

Attention map  ≠  bản giải thích hoàn chỉnh về reasoning của model

Dù vậy, attention map vẫn rất có giá trị để debug, quan sát pattern và hiểu cơ chế cục bộ của một head. Chỉ đừng biến nó thành lời giải thích cho toàn bộ model.


07Causal mask: vì sao LLM không được nhìn tương lai

Với language model autoregressive, mục tiêu là dự đoán token kế tiếp:

P( x_t | x_1, x_2, ..., x_(t-1) )

Khi tính hidden state tại vị trí t, model không được phép dùng x_(t+1), x_(t+2) và những token sau đó. Nếu được nhìn tương lai trong training, bài toán next-token prediction sẽ bị rò rỉ đáp án - model chỉ việc chép lại.

Vì vậy decoder-only LLM dùng causal mask. Với 4 token:

          Key position
          1   2   3   4
Query 1   v   x   x   x
      2   v   v   x   x
      3   v   v   v   x
      4   v   v   v   v

Dưới dạng ma trận, mask M có giá trị 0 ở ô được phép và âm vô cùng ở ô bị cấm:

M =  [  0   -inf  -inf  -inf ]
     [  0    0    -inf  -inf ]
     [  0    0     0    -inf ]
     [  0    0     0     0   ]

Mask được cộng vào score trước softmax:

A = softmax( Q Kᵀ / căn(d_k) + M )

Vì e mũ âm vô cùng bằng 0, các vị trí tương lai nhận attention weight đúng bằng 0.

~/diagrams/attention-self-attention/attention-mask.htmltương tác
mở riêng ↗
HìnhCả hai loại mask đều làm việc ở cùng một chỗ - cộng âm vô cùng vào score trước khi softmax chạy - nhưng giải quyết hai vấn đề khác nhau. Causal mask chặn thông tin đến từ tương lai, padding mask chặn những ô đệm không chứa dữ liệu thật. Hai kết quả ở bên phải là hai điều dễ nhầm nhất: ô bị mask nhận trọng số đúng bằng 0, và mask không hề xoá token khỏi chuỗi nên training vẫn chạy song song.
đường đi của scorehai loại maskhệ quả của mask

Điểm quan trọng: causal masking không xoá token khỏi sequence. Model vẫn tính toàn bộ sequence song song trong training. Mask chỉ ngăn mỗi Query truy cập những Key không hợp lệ. Đây là một trong những lợi thế lớn so với RNN: dù dependency là autoregressive, phần lớn phép toán trong training Transformer vẫn vectorize và chạy song song được.

Ngoài causal mask còn có padding mask. Khi nhiều sequence độ dài khác nhau được pad về cùng một shape, padding mask ngăn model attention vào những token đệm giả.

Causal mask   ->  chặn thông tin từ tương lai
Padding mask  ->  chặn vị trí không chứa dữ liệu thật

Một lưu ý rất thực dụng khi implement: convention boolean của mỗi framework một khác. Trong một số API, True nghĩa là “được phép attend”; trong API khác, True lại nghĩa là “bị mask”. Đọc đúng semantics của hàm đang dùng, đừng chỉ nhìn shape của mask rồi cho là xong.


08Multi-Head Attention: vì sao không chỉ dùng một head

Một attention head chỉ tạo ra một bộ Q, K, V projection và một attention distribution tại mỗi position. Transformer dùng nhiều head chạy song song.

Với head thứ i:

Q_i = X W_i^Q
K_i = X W_i^K
V_i = X W_i^V

head_i = Attention(Q_i, K_i, V_i)

Các head được nối lại rồi chiếu qua W_O:

H = Concat(head_1, head_2, ..., head_h)
MultiHead(X) = H W_O
~/diagrams/attention-self-attention/multi-head.htmltương tác
mở riêng ↗
HìnhMỗi làn là một head với bộ W_Q, W_K, W_V riêng, nên mỗi head khớp trong một representation subspace khác nhau. Bốn kết quả được nối lại rồi mới đi qua W_O - đó mới là chỗ thông tin giữa các head được trộn. Điều đáng nhớ là kiến trúc không phân công trước cho head nào cả; việc một head nghiêng về quan hệ gần hay phụ thuộc xa là chuyện emergent trong training.
đường của head chínhcác head song songchỗ các head được trộn

Tại sao chuyện này hữu ích?

Mỗi head có projection matrix riêng, nghĩa là mỗi head có thể so khớp trong một representation subspace khác nhau. Một head có thể đặt attention mạnh vào quan hệ gần, một head khác học dependency xa, một head khác nữa tập trung vào dấu câu hoặc một cấu trúc nào đó. Quan trọng: đây là khả năng của multi-head attention, không phải phân công cố định do kiến trúc đặt ra.

Trong Transformer gốc:

d_k = d_v = d_model / h

Ví dụ:

d_model = 512
h       = 8 head
->  d_k = d_v = 64 cho mỗi head

Nhờ giảm số chiều của mỗi head, nhiều attention operation chạy song song được mà tổng chi phí vẫn ở cùng bậc với một attention full-dimension.

Có thể hình dung multi-head như việc một token đặt nhiều câu hỏi khác nhau về cùng một ngữ cảnh:

Token hiện tại
  |-- Head 1: có token nào liên quan theo pattern A?
  |-- Head 2: có token nào liên quan theo pattern B?
  |-- Head 3: có token nào liên quan theo pattern C?
  +-- Head 4: có token nào liên quan theo pattern D?
        ->  ghép các kết quả
        ->  representation phong phú hơn

Multi-head attention vì thế không phải “chạy cùng một attention nhiều lần rồi lấy trung bình”, mà là nhiều phép chiếu Q/K/V được học độc lập rồi ghép kết quả lại.


09Self-Attention nằm ở đâu, và vị trí đi vào bằng cách nào

Self-Attention là thành phần trung tâm, nhưng Self-Attention không phải toàn bộ Transformer.

~/diagrams/attention-self-attention/transformer-block.htmltương tác
mở riêng ↗
HìnhMột block gồm bốn phần với bốn việc khác nhau: attention trao đổi thông tin giữa các vị trí, MLP biến đổi feature tại từng vị trí, residual giữ một đường truyền thẳng xuyên qua layer, normalization kiểm soát scale. Nhánh dưới là thứ attention không tự có - nếu không được cho biết vị trí, phép attention dựa trên nội dung sẽ coi câu như một tập hợp không thứ tự.
đường đi trong blockresidual và normthông tin vị trí

Trong các kiến trúc hiện đại, thứ tự normalization có thể khác Transformer gốc - ví dụ Pre-Norm thay vì Post-Norm - nhưng ý tưởng lớn giữ nguyên:

  • Attention: trao đổi thông tin giữa các vị trí.
  • MLP/FFN: biến đổi feature tại từng vị trí một cách độc lập.
  • Residual connection: giữ đường truyền thông tin trực tiếp xuyên qua layer.
  • Normalization: kiểm soát scale và giúp optimization ổn định.

Một điểm cực kỳ quan trọng: self-attention thuần tuý không tự biết thứ tự token.

Nếu chỉ đưa vào một tập vector mà không có bất kỳ tín hiệu vị trí nào, phép attention dựa trên nội dung không có cơ chế nội tại nào để biết token nào ở vị trí 1, vị trí 2 hay vị trí 100. Đảo thứ tự các token rồi tính lại sẽ cho ra cùng một tập kết quả. Vì vậy Transformer phải đưa thông tin vị trí vào representation, hoặc đưa thẳng vào phép tính attention.

Transformer gốc cộng sinusoidal positional encoding vào embedding:

Token embedding
      +
Positional encoding
      ->  input vào Transformer

Các LLM hiện đại có thể làm khác. Một ví dụ phổ biến là Rotary Position Embedding (RoPE), trong đó thông tin vị trí được đưa vào Query và Key thông qua một phép quay trong không gian vector, khiến attention score biểu diễn quan hệ vị trí tương đối theo cách khác hẳn phép cộng sinusoidal truyền thống. RoPE là một chủ đề đủ lớn để tách riêng; bài về embedding đã đi qua bốn cách mã hoá vị trí và cái giá của từng cách.

Điều cần giữ lại ở đây:

Attention trả lời “token nào liên quan?”, còn cơ chế vị trí cung cấp thông tin để model biết “token nằm ở đâu và quan hệ vị trí giữa chúng là gì”.

Đến đây có thể nối lại toàn bộ pipeline:

Text
  ->  Tokens
  ->  Embeddings
  ->  cộng thông tin vị trí
  ->  Q / K / V projections
  ->  Self-Attention
  ->  hidden states đã mang ngữ cảnh
  ->  MLP / FFN
  ->  block tiếp theo

Một LLM xếp chồng nhiều block như vậy. Qua mỗi block, hidden representation được cập nhật thêm một lần. Vì thế “model hiểu ngữ cảnh” không xảy ra trong một phép attention duy nhất; nó là kết quả của nhiều vòng communication cộng transformation qua nhiều layer.


10Cái giá của Self-Attention: vì sao context dài lại đắt

Phần cốt lõi tạo ra attention score là Q Kᵀ. Nếu sequence có n token thì score matrix có cỡ n × n, và số cặp token cần xét tăng theo n bình phương.

Sequence length Số phần tử trong một attention matrix
1.024 1.048.576
4.096 16.777.216
16.384 268.435.456
32.768 1.073.741.824

Con số này là cho mỗi head, trước khi tính đến batch và số layer, nếu hình dung attention matrix được dựng đầy đủ trong bộ nhớ.

~/diagrams/attention-self-attention/attention-cost.htmltương tác
mở riêng ↗
HìnhĐộ dài chuỗi quyết định số cặp token phải xét, và số cặp đó tăng theo bình phương. Chi phí rẽ làm hai nhánh - compute và lưu lượng bộ nhớ - và mỗi nhánh dẫn tới một hướng tối ưu khác nhau. Nhánh trên đổi chính pattern attention nên kết quả cũng đổi theo; nhánh dưới giữ nguyên exact attention và chỉ thay cách schedule tính toán.
chi phí theo độ dàihai loại chi phíhai hướng tối ưu

Về computational complexity của phần self-attention cốt lõi, Transformer gốc mô tả bậc O(n² d), trong đó n là sequence length và d là số chiều representation.

Cần lưu ý rằng một Transformer layer đầy đủ còn có chi phí của Q/K/V projection, output projection và FFN - những phần này thường chứa các term theo O(n d²). Vì vậy câu “Transformer luôn có cost bậc hai” là cách nói rút gọn về tương tác attention theo sequence length, không phải toàn bộ FLOPs của mọi thành phần trong block.

Đây là lý do context window dài trở thành một bài toán hệ thống lớn.

Một bước tiến quan trọng là FlashAttention. Điểm cần hiểu đúng: ở phiên bản exact, FlashAttention không thay công thức attention bằng một xấp xỉ khác. Nó tổ chức lại cách tính theo hướng IO-aware, dùng tiling để giảm số lần đọc/ghi giữa các cấp bộ nhớ GPU, đặc biệt giữa HBM và SRAM trên chip.

Cách hình dung tiêu chuẩn
Q Kᵀ -> dựng cả score matrix -> softmax -> dựng cả weight matrix -> nhân V

Ý tưởng kiểu FlashAttention
chia Q/K/V thành block
  -> tính từng tile
  -> cập nhật softmax và output theo block
  -> tránh dựng toàn bộ attention matrix trong HBM

Nói cách khác:

Toán học của exact attention:            giữ nguyên
Cách schedule computation và memory:     thay đổi mạnh

Đây là một phân biệt rất quan trọng. Khi nói “attention optimization”, có hai hướng khác hẳn nhau:

1. Thay thuật toán hoặc attention pattern
   -> sparse attention, local attention, approximate attention

2. Giữ exact attention nhưng tính thông minh hơn
   -> FlashAttention và các fused SDPA kernel

Những chủ đề như FlashAttention, KV Cache, MQA/GQA, sliding-window attention hay long-context optimization xứng đáng được đào sâu ở các bài về inference optimization và context window. Ở đây chỉ cần giữ một ý: tương tác all-pairs bậc hai là một trong những nguyên nhân chính khiến context dài tốn tài nguyên.


11Ghép tất cả lại: một token đi qua Self-Attention

Nếu phải rút cả bài thành bốn dòng, đó là:

Q = X W_Q,  K = X W_K,  V = X W_V
S = Q Kᵀ / căn(d_k)
A = softmax(S + M)
O = A V

Trong đó:

Q  ->  tôi cần gì?
K  ->  tôi có phù hợp không?
V  ->  tôi sẽ cung cấp thông tin gì?
S  ->  mức độ tương thích
M  ->  vị trí nào bị cấm
A  ->  attention weights
O  ->  representation mới sau khi lấy ngữ cảnh

Với Multi-Head Attention, toàn bộ quá trình đó diễn ra nhiều lần song song với những projection khác nhau, rồi mới được ghép lại và chiếu qua W_O. Sau đó tới residual, normalization, MLP, và block kế tiếp - lặp lại cho tới khi model có được hidden representation đủ hữu ích để đoán token tiếp theo.


12Những hiểu lầm thường gặp

Hiểu lầm Thực tế
“Attention chỉ chọn một token quan trọng nhất” softmax thường tạo phân phối trên nhiều token; output là weighted sum của nhiều Value
“Q, K, V là ba embedding cố định của mỗi token” chúng được tạo động từ hidden state bằng các learned projection, tính lại mỗi lần chạy
“Self-Attention nghĩa là token chỉ attention vào chính nó” self nghĩa là Q, K, V đến từ cùng một sequence; token vẫn attention sang nhiều token khác
“Attention weight cao thì token đó chắc chắn là nguyên nhân” weight mô tả một phép trộn cục bộ trong một head, không phải causal explanation của cả model
“Causal attention buộc Transformer xử lý tuần tự khi training” mask chặn thông tin tương lai nhưng nhiều position vẫn vectorize và tính song song được
“Multi-head attention là nhiều bản copy giống hệt nhau” mỗi head có Q/K/V projection riêng và học được các attention pattern khác nhau
“Self-attention tự biết thứ tự từ” không; cần positional encoding, learned position embedding, RoPE hoặc cơ chế tương đương
“FlashAttention là một loại attention gần đúng” bản exact tính đúng cùng kết quả, chỉ khác ở cách tổ chức IO; sparse hay approximate là chuyện khác
“Chia căn d_k là một thủ thuật tuỳ ý” nó gắn trực tiếp với phương sai của tích vô hướng d_k chiều
“Cost của Transformer luôn là bậc hai” bậc hai là chi phí attention theo độ dài; projection và FFN đóng góp các term theo d bình phương

Attention là chỗ token thôi đứng một mình

Self-Attention là cầu nối giữa hai ý tưởng quan trọng nhất trong LLM:

Token có một vector
        ->
Vector đó cần thay đổi theo ngữ cảnh

Embedding cung cấp representation ban đầu. Self-Attention cho phép mỗi position truy vấn, đánh giá và lấy thông tin từ những position khác. Multi-Head mở rộng quá trình đó sang nhiều representation subspace. Causal mask đảm bảo language model không nhìn tương lai. Cơ chế vị trí cung cấp trật tự. Nhiều Transformer block xếp chồng lặp lại vòng communication và transformation đó cho tới khi model tạo được hidden representation đủ tốt để đoán token kế tiếp.

Những gì nên mang theo:

  • attention là tra cứu mềm theo nội dung, không phải tra cứu cứng theo địa chỉ;
  • Key quyết định mức liên quan, Value mang nội dung, và hai vai trò đó cố tình tách nhau;
  • Q, K, V được sinh từ hidden state bằng ba ma trận học được, không phải ba bảng lưu sẵn;
  • chia căn d_k là để softmax không rơi vào vùng gradient gần 0;
  • mask cộng vào score trước softmax, và nó không xoá token khỏi sequence;
  • attention map là công cụ debug tốt, không phải lời giải thích đầy đủ;
  • multi-head là nhiều phép chiếu độc lập, không phải nhiều bản sao;
  • attention không tự biết thứ tự, và tương tác all-pairs là nguồn gốc của chi phí bậc hai.

Từ đây, những khái niệm ở các bài sau - KV Cache, context window, long-context attention, inference optimization - sẽ trở nên tự nhiên hơn nhiều. Tất cả đều bắt đầu từ đúng một câu hỏi: Q đang tương tác với K và V như thế nào.


Tài liệu tham khảo

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.