Nền tảng Mô hình Ngôn ngữ Lớn Học qua Thực hành

bởi David Louapre

Chương 6

Các Kiến trúc Thay thế: Mixture of Experts, Attention Tuyến tính và Mamba (SSM)

Làm thế nào để mở rộng quy mô tham số mà không tăng chi phí tính toán với Sparse MoE, và vượt qua giới hạn bậc hai của Attention bằng mô hình không gian trạng thái (Mamba).

Đây là Chương 6 trong chuỗi bài giảng tương tác “Nền tảng Mô hình Ngôn ngữ Lớn — Học qua Thực hành”. Ở các chương trước, ta đã lắp ráp hoàn chỉnh khối Transformer chuẩn (Chương 2), huấn luyện nó theo định luật mở rộng Chinchilla (Chương 3) và tối ưu hóa tốc độ suy luận với KV Cache, GQA, FlashAttention (Chương 5).

Tuy nhiên, kiến trúc Transformer “đặc” (Dense Transformer) vẫn vấp phải hai bức tường vật lý lớn: (1) mỗi token đi qua đều buộc toàn bộ hàng chục tỷ tham số phải tính toán, và (2) cơ chế Softmax Attention đòi hỏi chi phí tính toán bậc hai O(L2)\mathcal{O}(L^2) cùng bộ nhớ KV Cache phình to tuyến tính theo chiều dài chuỗi LL. Trong chương này, bạn sẽ tự tay cài đặt ba đột phá kiến trúc giúp phá vỡ hai giới hạn đó: Hỗn hợp Chuyên gia Thưa (Sparse MoE), Attention Tuyến tính (Linear Attention) và Mô hình Không gian Trạng thái Chọn lọc (Mamba / SSM).

Khi đọc một câu hỏi về lập trình Python, liệu một mô hình ngôn ngữ có thực sự cần kích hoạt các nơ-ron lưu trữ kiến thức về ẩm thực Pháp hay lịch sử La Mã? Và khi đọc một cuốn sách dài 500.000500.000 token, liệu mô hình có bắt buộc phải giữ lại nguyên vẹn từng cặp vector khóa – giá trị (kt,vt)(k_t, v_t) của mọi từ từ trang đầu tiên?

Hai trăn trở đó dẫn dắt toàn bộ cuộc cách mạng kiến trúc hậu-Transformer xoay quanh hai câu hỏi cốt lõi:

Tập dữ liệu & Mô hình thực nghiệm

Trong chương này, toàn bộ bài tập thực hành được thiết kế để chạy tức thì trên CPU chỉ với PyTorch thuần (dưới 10 giây mỗi bài), giúp bạn kiểm chứng trực tiếp bản chất toán học của từng kiến trúc:

  • Mô phỏng Định tuyến Chuyên gia (SparseMoELayer): Khảo sát hiện tượng sụp đổ định tuyến (routing collapse) và tác dụng cân bằng tải của hàm mất mát phụ trợ Laux\mathcal{L}_{\text{aux}} (Fedus & Shazeer, 2022; Shazeer & others, 2017).
  • Đối ngẫu Song song – Truy hồi (CausalLinearAttention): Kiểm chứng sự trùng khớp số học tuyệt đối (sai số <10−6< 10^{-6}) giữa phép nhân ma trận song song O(L2d)\mathcal{O}(L^2 d) và phép cập nhật trạng thái truy hồi O(Ld2)\mathcal{O}(L d^2) (Katharopoulos & others, 2020).
  • Bài toán Sao chép Chọn lọc (Selective Copying): Thử thách kinh điển của bài báo Mamba (Gu & Dao, 2023), chứng minh tại sao các hệ không gian trạng thái bất biến thời gian (LTI / S4) thất bại trong khi cơ chế quét chọn lọc (Selective Scan) giải quyết hoàn hảo.

1. Hỗn hợp Chuyên gia Thưa (Sparse Mixture of Experts — MoE)

Trong Chương 2 (Mục 5), khi phân rã số lượng tham số của một khối Transformer chuẩn, ta đã thấy rằng các lớp mạng truyền thẳng (Feed-Forward Network — FFN / SwiGLU) chiếm tới 2/3 tổng số tham số của mô hình. Trong một mô hình đặc (Dense model) như Llama-3 70B, mọi token — dù là dấu chấm câu , hay một thuật ngữ chuyên ngành phức tạp — đều phải nhân ma trận qua toàn bộ các trọng số FFN này.

1.1 Từ mạng FFN đặc đến tính toán có điều kiện (Conditional Computation)

Ý tưởng của Hỗn hợp Chuyên gia Thưa (Sparse Mixture of Experts — MoE) là thay thế một lớp FFN khổng lồ duy nhất bằng EE mạng FFN độc lập nhỏ hơn gọi là các chuyên gia (experts) {E1,E2,…,EE}\{E_1, E_2, \dots, E_E\}, đi kèm một mạng cổng nhẹ gọi là bộ định tuyến (router hoặc gating network).

Với vector biểu diễn x∈Rdmodelx \in \mathbb{R}^{d_{\text{model}}} của một token ở đầu vào lớp FFN, bộ định tuyến dùng ma trận trọng số Wg∈Rdmodel×EW_g \in \mathbb{R}^{d_{\text{model}} \times E} để tính điểm tương thích cho từng chuyên gia, sau đó chỉ kích hoạt Top-kk chuyên gia có điểm cao nhất (thường k=1k = 1 hoặc k=2k = 2, với k≪Ek \ll E):

pi(x)=exp⁡(xWg,i)∑j=1Eexp⁡(xWg,j),Gi(x)={pi(x)∑j∈Tk(x)pj(x)neˆˊu i∈Tk(x),0ngược lại,p_i(x) = \frac{\exp(x W_{g, i})}{\sum_{j=1}^{E} \exp(x W_{g, j})}, \qquad G_i(x) = \begin{cases} \dfrac{p_i(x)}{\sum_{j \in \mathcal{T}_k(x)} p_j(x)} & \text{nếu } i \in \mathcal{T}_k(x), \\[8pt] 0 & \text{ngược lại}, \end{cases}

trong đó Tk(x)\mathcal{T}_k(x) là tập hợp chỉ số của kk chuyên gia có xác suất pi(x)p_i(x) lớn nhất. Đầu ra của lớp MoE là tổng có trọng số chỉ trên kk chuyên gia được chọn:

y=∑i∈Tk(x)Gi(x) Ei(x).y = \sum_{i \in \mathcal{T}_k(x)} G_i(x)\, E_i(x).

Vì Gi(x)=0G_i(x) = 0 với E−kE - k chuyên gia còn lại, ta không cần thực hiện phép nhân ma trận của các chuyên gia đó! Nhờ vậy, ta có thể tăng số lượng chuyên gia EE lên 8, 64, hay 256 để mở rộng dung lượng ghi nhớ tri thức của mô hình mà chi phí FLOPs trên mỗi token chỉ tương đương với việc chạy kk chuyên gia.

1.2 Sụp đổ định tuyến (Routing Collapse) và Hàm mất mát cân bằng tải

Tuy nhiên, khi huấn luyện lớp Sparse MoE bằng hạ gradient thông thường, một hiện tượng bệnh lý gần như chắc chắn sẽ xảy ra: sụp đổ định tuyến (routing collapse).

Nếu ở những bước khởi tạo đầu tiên, chuyên gia E1E_1 tình cờ có trọng số nhỉnh hơn một chút và được bộ định tuyến chọn nhiều hơn, thì E1E_1 sẽ nhận được nhiều cập nhật gradient hơn các chuyên gia khác. Kết quả là E1E_1 học nhanh hơn, dự đoán tốt hơn, và bộ định tuyến lại càng tăng xác suất p1(x)p_1(x) gửi token vào E1E_1! Chỉ sau vài trăm bước, toàn bộ token đều dồn vào 1–2 chuyên gia (“hiệu ứng người giàu càng giàu”), trong khi E−2E - 2 chuyên gia còn lại bị bỏ hoang hoàn toàn (“chuyên gia chết” — dead experts), biến mô hình MoE đắt đỏ thành một mô hình đặc cỡ nhỏ.

Để ngăn chặn hiệu ứng tự củng cố này, Switch Transformer (Fedus & Shazeer, 2022) chuẩn hóa một hàm mất mát cân bằng tải phụ trợ (load-balancing auxiliary loss) cực kỳ thanh lịch. Xét một batch gồm NN token:

  1. Gọi fif_i là tỷ lệ token thực sự được định tuyến tới chuyên gia ii: fi=1kN∑x∈BatchI(i∈Tk(x)),với ∑i=1Efi=1.f_i = \frac{1}{k N} \sum_{x \in \text{Batch}} \mathbb{I}\big(i \in \mathcal{T}_k(x)\big), \qquad \text{với } \sum_{i=1}^{E} f_i = 1.
  2. Gọi PiP_i là xác suất định tuyến trung bình mà cổng Softmax phân bổ cho chuyên gia ii trên toàn batch: Pi=1N∑x∈Batchpi(x),với ∑i=1EPi=1.P_i = \frac{1}{N} \sum_{x \in \text{Batch}} p_i(x), \qquad \text{với } \sum_{i=1}^{E} P_i = 1.

Vì hàm chỉ thị I(⋅)\mathbb{I}(\cdot) trong fif_i không khả vi, ta nhân fif_i với PiP_i (khả vi theo trọng số cổng WgW_g) để tạo ra hàm mất mát phụ trợ:

Laux=E∑i=1Efi⋅Pi,Ltotal=LLM+α Laux.\mathcal{L}_{\text{aux}} = E \sum_{i=1}^{E} f_i \cdot P_i, \qquad \mathcal{L}_{\text{total}} = \mathcal{L}_{\text{LM}} + \alpha \, \mathcal{L}_{\text{aux}}.

Tại sao công thức này hoạt động? Khi cả 8 chuyên gia được chia tải đồng đều hoàn hảo, ta có fi=1Ef_i = \frac{1}{E} và Pi=1EP_i = \frac{1}{E} với mọi ii, suy ra:

Lauxmin⁡=E∑i=1E1E⋅1E=1.\mathcal{L}_{\text{aux}}^{\min} = E \sum_{i=1}^{E} \frac{1}{E} \cdot \frac{1}{E} = 1.

Ngược lại, nếu toàn bộ token bị dồn vào một chuyên gia duy nhất (f1=1,P1=1f_1 = 1, P_1 = 1), hàm mất mát sẽ vọt lên Laux=E⋅1=E\mathcal{L}_{\text{aux}} = E \cdot 1 = E. Do tích vô hướng ∑fiPi\sum f_i P_i phạt nặng những chuyên gia vừa có tần suất được chọn fif_i cao vừa có xác suất PiP_i lớn, gradient của Laux\mathcal{L}_{\text{aux}} sẽ lập tức kéo giảm logit của các chuyên gia quá tải và đẩy token sang các chuyên gia đang rảnh rỗi.

1.3 Từ Mixtral 8x7B đến Chuyên gia dùng chung (Shared Experts) trong DeepSeek-V3

Trong thực tế hiện đại, kiến trúc Sparse MoE đã phát triển qua hai thế hệ tiêu biểu:

y=x+Eshared(x)+∑i∈Tk(x)Gi(x) Ei(x).y = x + E_{\text{shared}}(x) + \sum_{i \in \mathcal{T}_k(x)} G_i(x)\, E_i(x).

Nhờ có Eshared(x)E_{\text{shared}}(x) gánh vác các tri thức ngữ pháp và từ vựng phổ quát, các chuyên gia định tuyến EiE_i không cần phải học lặp lại kiến thức cơ bản mà có thể tập trung 100% dung lượng vào các lĩnh vực chuyên biệt. Ngoài ra, DeepSeek-V3 còn thay thế hàm phạt Laux\mathcal{L}_{\text{aux}} bằng cơ chế tự động điều chỉnh độ lệch định tuyến (auxiliary-loss-free load balancing), tránh việc hàm phạt phụ trợ làm suy giảm chất lượng ngôn ngữ.

Hãy tương tác với sơ đồ dưới đây để quan sát cách bộ định tuyến Top-2 phân phối các token của một đoạn mã Python vào 8 chuyên gia, và điều gì xảy ra khi bạn tắt hàm mất mát cân bằng tải:

Bộ định tuyến Hỗn hợp Chuyên gia Thưa (Sparse MoE Router)
Mỗi token đi qua cổng định tuyến G(x) để chọn 2 trong 8 chuyên gia chuyên biệt, đồng thời đi qua 1 chuyên gia dùng chung (Shared Expert). Bật/tắt Load Balancing Loss để quan sát hiện tượng sụp đổ định tuyến (routing collapse) khi mọi token dồn vào E2 và E5.

Bây giờ, hãy tự tay cài đặt lớp SparseMoELayer bằng PyTorch và kiểm chứng bằng thực nghiệm sự khác biệt giữa huấn luyện có và không có Laux\mathcal{L}_{\text{aux}}.

Bài tập 6.1 Cài đặt lớp Sparse Top-2 MoE và Cân bằng tải
Dữ liệu: Tổng hợp (PyTorch) · Thời gian chạy: < 1 phút

Cài đặt mô-đun SparseMoELayer gồm E=8E = 8 chuyên gia (mỗi chuyên gia là một mạng 2 lớp tuyến tính với hàm kích hoạt SiLU) và bộ định tuyến chọn k=2k = 2 chuyên gia có xác suất cao nhất cho mỗi token:

  1. Chuẩn hóa lại trọng số của 2 chuyên gia được chọn sao cho tổng bằng 11, và tính đầu ra tổ hợp thưa yy.
  2. Tính hàm mất mát cân bằng tải phụ trợ Laux=E∑i=1EfiPi\mathcal{L}_{\text{aux}} = E \sum_{i=1}^{E} f_i P_i.
  3. Khởi tạo trọng số bộ định tuyến có một chút thiên lệch ban đầu về chuyên gia 0 và 1 (như trong thực tế khi có nhiễu ngẫu nhiên), sau đó huấn luyện lớp MoE trong 80 bước với α=0\alpha = 0 (tắt cân bằng tải) và α=0.05\alpha = 0.05 (bật cân bằng tải). So sánh phân bố tỷ lệ chọn fif_i của 8 chuyên gia ở hai chế độ.
Mã khởi tạo
import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(42)
B, L, d_model, d_ff = 16, 32, 64, 128
num_experts, top_k = 8, 2

# Tạo batch token đầu vào và nhãn mục tiêu giả lập
x = torch.randn(B, L, d_model)
target = torch.randn(B, L, d_model)
print(f"Input shape: {tuple(x.shape)} ({B * L} tokens)")
Input shape: (16, 32, 64) (512 tokens)
Hint

Trải phẳng đầu vào x từ (B, L, d_model) thành x_flat có kích thước (N, d_model) với N = B * L. Tính xác suất cổng probs = F.softmax(self.router(x_flat), dim=-1) có kích thước (N, E), rồi dùng torch.topk(probs, self.k, dim=-1) để lấy top_vals và top_idx kích thước (N, k).

Hint

Để tính tần suất fif_i, dùng F.one_hot(top_idx, self.E).float() thu được tensor (N, k, E). Lấy tổng theo chiều k (dim=1), trung bình theo chiều N (dim=0) và chia cho self.k để ∑ifi=1\sum_i f_i = 1. Vector xác suất trung bình là P_i = probs.mean(dim=0).

Điểm kiểm tra kết quả bạn cần đạt được

Khi α=0.0\alpha = 0.0, hiện tượng sụp đổ định tuyến xảy ra rõ rệt: hai chuyên gia đầu tiên chiếm tới hơn 91%91\% tổng lưu lượng (56.2%56.2\% và 35.4%35.4\%), đẩy Laux\mathcal{L}_{\text{aux}} lên 3.6853.685 và các chuyên gia từ 5 đến 7 gần như bị bỏ đói (<1%< 1\%). Khi bật α=0.05\alpha = 0.05, tải được chia đều quanh mức lý tưởng 12.5%12.5\% cho cả 8 chuyên gia và Laux\mathcal{L}_{\text{aux}} hội tụ sát về 1.0021.002.

=== Không có Load Balancing Loss (alpha = 0.0) ===
Tỷ lệ chọn f_i (%): [56.2, 35.4, 3.1, 2.0, 1.4, 0.8, 0.6, 0.6]
L_aux cuối: 3.685 | Độ lệch chuẩn tải: 20.1%

=== Có Load Balancing Loss (alpha = 0.05) ===
Tỷ lệ chọn f_i (%): [13.1, 12.5, 12.7, 12.1, 12.3, 12.9, 12.1, 12.3]
L_aux cuối: 1.002 | Độ lệch chuẩn tải: 0.4%
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(42)
B, L, d_model, d_ff = 16, 32, 64, 128
num_experts, top_k = 8, 2

# Tạo batch token đầu vào và nhãn mục tiêu giả lập
x = torch.randn(B, L, d_model)
target = torch.randn(B, L, d_model)
print(f"Input shape: {tuple(x.shape)} ({B * L} tokens)")
Input shape: (16, 32, 64) (512 tokens)
class SparseMoELayer(nn.Module):
    def __init__(self, d_model, d_ff, num_experts=8, top_k=2):
        super().__init__()
        self.E = num_experts
        self.k = top_k
        self.router = nn.Linear(d_model, num_experts, bias=False)
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(d_model, d_ff),
                nn.SiLU(),
                nn.Linear(d_ff, d_model),
            )
            for _ in range(num_experts)
        ])

    def forward(self, x):
        N, D = x.numel() // x.size(-1), x.size(-1)
        x_flat = x.view(N, D)
        probs = F.softmax(self.router(x_flat), dim=-1)  # (N, E)
        top_vals, top_idx = torch.topk(probs, self.k, dim=-1)
        weights = top_vals / top_vals.sum(dim=-1, keepdim=True)

        # (1) Hàm mất mát cân bằng tải L_aux = E * sum(f_i * P_i)
        one_hot = F.one_hot(top_idx, self.E).float()    # (N, k, E)
        f_i = one_hot.sum(dim=1).mean(dim=0) / self.k   # (E,)
        P_i = probs.mean(dim=0)                         # (E,)
        aux_loss = self.E * torch.sum(f_i * P_i)

        # (2) Tính đầu ra tổ hợp thưa từ k chuyên gia được chọn
        out = torch.zeros_like(x_flat)
        for slot in range(self.k):
            idx_s = top_idx[:, slot]
            w_s = weights[:, slot].unsqueeze(-1)
            for e in range(self.E):
                mask = idx_s == e
                if mask.any():
                    out[mask] += w_s[mask] * self.experts[e](
                        x_flat[mask]
                    )
        return out.view_as(x), aux_loss, f_i.detach()


def run_experiment(alpha, title):
    torch.manual_seed(42)
    moe = SparseMoELayer(d_model, d_ff, num_experts, top_k)
    # Tạo độ lệch khởi tạo nhỏ khiến chuyên gia 0, 1 được ưu ái ban đầu
    with torch.no_grad():
        moe.router.weight[0] *= 1.8
        moe.router.weight[1] *= 1.5
    opt = torch.optim.Adam(moe.parameters(), lr=1e-2)
    for _ in range(80):
        out, aux_loss, f_i = moe(x)
        loss = F.mse_loss(out, target) + alpha * aux_loss
        opt.zero_grad()
        loss.backward()
        opt.step()
    pct = [round(v * 100, 1) for v in f_i.tolist()]
    std = (f_i * 100).std().item()
    print(f"=== {title} ===")
    print(f"Tỷ lệ chọn f_i (%): {pct}")
    print(f"L_aux cuối: {aux_loss.item():.3f} | Độ lệch chuẩn tải: {std:.1f}%")


run_experiment(0.0, "Không có Load Balancing Loss (alpha = 0.0)")
print()
run_experiment(0.05, "Có Load Balancing Loss (alpha = 0.05)")
=== Không có Load Balancing Loss (alpha = 0.0) ===
Tỷ lệ chọn f_i (%): [56.2, 35.4, 3.1, 2.0, 1.4, 0.8, 0.6, 0.6]
L_aux cuối: 3.685 | Độ lệch chuẩn tải: 20.1%

=== Có Load Balancing Loss (alpha = 0.05) ===
Tỷ lệ chọn f_i (%): [13.1, 12.5, 12.7, 12.1, 12.3, 12.9, 12.1, 12.3]
L_aux cuối: 1.002 | Độ lệch chuẩn tải: 0.4%
Điều cần lưu ý

Hãy chú ý cách hàm mất mát Laux=E∑fiPi\mathcal{L}_{\text{aux}} = E \sum f_i P_i vượt qua rào cản không khả vi của phép chọn rời rạc topk: đạo hàm không đi qua tần suất đếm fif_i (vốn là hằng số từng khúc), mà đi qua xác suất trung bình PiP_i, với fif_i đóng vai trò là hệ số phạt động tỉ lệ thuận với mức độ quá tải thực tế của chuyên gia ii. Sparse MoE cho phép mô hình mở rộng quy mô tham số lên hàng trăm tỷ mà chi phí tính toán mỗi bước chỉ bằng một phần nhỏ.


2. Attention Tuyến tính và Sự đối ngẫu RNN–Attention (Linear Attention)

Sparse MoE giúp giảm chi phí tại các lớp FFN, nhưng còn bản thân lớp Self-Attention thì sao? Khi chiều dài chuỗi LL tăng lên 128K128\text{K} hay 1M1\text{M} token, ma trận điểm chú ý QK⊤∈RL×LQ K^\top \in \mathbb{R}^{L \times L} đòi hỏi số phép tính tăng theo bậc hai O(L2d)\mathcal{O}(L^2 d), và trong lúc sinh văn bản tự hồi quy, bộ nhớ KV Cache phải lưu toàn bộ các cặp (k1,v1),…,(kt,vt)(k_1, v_1), \dots, (k_t, v_t), tốn thời gian đọc bộ nhớ O(t)\mathcal{O}(t) ở bước thứ tt.

Tại sao Self-Attention lại bị khóa chặt vào độ phức tạp O(L2)\mathcal{O}(L^2)? Thủ phạm nằm ở hàm mũ phi tuyến Softmax bao quanh tích vô hướng qt⊤kjq_t^\top k_j.

2.1 Phép ma thuật của tính kết hợp phép nhân ma trận

Trong cơ chế Causal Self-Attention chuẩn, đầu ra yt∈Rdy_t \in \mathbb{R}^d tại vị trí tt là trung bình có trọng số của các vector giá trị vjv_j (j≤tj \le t):

yt=∑j=1tsim(qt,kj) vj∑j=1tsim(qt,kj),với simsoftmax(qt,kj)=exp⁡ ⁣(qt⊤kjd).y_t = \frac{\sum_{j=1}^{t} \text{sim}(q_t, k_j)\, v_j}{\sum_{j=1}^{t} \text{sim}(q_t, k_j)}, \qquad \text{với } \text{sim}_{\text{softmax}}(q_t, k_j) = \exp\!\left(\frac{q_t^\top k_j}{\sqrt{d}}\right).

Vì hàm exp⁡(qt⊤kj)\exp(q_t^\top k_j) không thể tách thành tích độc lập của một hàm theo qtq_t và một hàm theo kjk_j (trừ khi khai triển chuỗi Taylor vô hạn), ta bắt buộc phải tính tích vô hướng qt⊤kjq_t^\top k_j cho mọi cặp (t,j)(t, j) với j≤tj \le t, dẫn đến phép nhân ma trận theo thứ tự:

Softmax Attention:(QK⊤)⏟L×LV⟹Độ phức tạp O(L2d).\text{Softmax Attention:} \quad \underbrace{\big(Q K^\top\big)}_{L \times L} V \quad \Longrightarrow \quad \text{Độ phức tạp } \mathcal{O}(L^2 d).

Năm 2020, Katharopoulos et al. (Katharopoulos & others, 2020) đặt một câu hỏi táo bạo: điều gì sẽ xảy ra nếu ta thay hàm tương đồng exp⁡(qt⊤kj/d)\exp(q_t^\top k_j / \sqrt{d}) bằng một hàm nhân tách được (kernel feature map) sim(qt,kj)=ϕ(qt)⊤ϕ(kj)\text{sim}(q_t, k_j) = \phi(q_t)^\top \phi(k_j), trong đó ϕ:Rd→R+d\phi: \mathbb{R}^d \to \mathbb{R}_{+}^d là một hàm kích hoạt dương áp dụng theo từng phần tử, chẳng hạn ϕ(x)=elu(x)+1\phi(x) = \text{elu}(x) + 1?

Khi đó, nhờ tính phân phối và kết hợp của phép nhân ma trận, ta có thể đưa ϕ(qt)⊤\phi(q_t)^\top ra ngoài dấu tổng ∑j=1t\sum_{j=1}^{t}:

yt=∑j=1t(ϕ(qt)⊤ϕ(kj))vj⊤∑j=1tϕ(qt)⊤ϕ(kj)=ϕ(qt)⊤(∑j=1tϕ(kj) vj⊤)ϕ(qt)⊤(∑j=1tϕ(kj)).y_t = \frac{\sum_{j=1}^{t} \big(\phi(q_t)^\top \phi(k_j)\big) v_j^\top}{\sum_{j=1}^{t} \phi(q_t)^\top \phi(k_j)} = \frac{\phi(q_t)^\top \left(\sum_{j=1}^{t} \phi(k_j)\, v_j^\top\right)}{\phi(q_t)^\top \left(\sum_{j=1}^{t} \phi(k_j)\right)}.

Ở dạng ma trận toàn chuỗi (không mặt nạ), chỉ cần đổi vị trí dấu ngoặc đơn từ (ϕ(Q)ϕ(K)⊤)V\big(\phi(Q)\phi(K)^\top\big)V sang ϕ(Q)(ϕ(K)⊤V)\phi(Q)\big(\phi(K)^\top V\big), ma trận trung gian kích thước L×LL \times L biến mất và được thay bằng ma trận trung gian kích thước cố định d×dd \times d:

Linear Attention:ϕ(Q)(ϕ(K)⊤V)⏟d×d⟹Độ phức tạp O(Ld2).\text{Linear Attention:} \quad \phi(Q) \underbrace{\big(\phi(K)^\top V\big)}_{d \times d} \quad \Longrightarrow \quad \text{Độ phức tạp } \mathcal{O}(L d^2).

Khi L≫dL \gg d (ví dụ chiều dài ngữ cảnh L=65.536L = 65.536 trong khi chiều một đầu chú ý chỉ là d=64d = 64), thay L2dL^2 d bằng Ld2L d^2 giúp giảm số phép tính đi hơn 1.000 lần!

2.2 Biểu diễn Truy hồi Trạng thái St∈Rd×dS_t \in \mathbb{R}^{d \times d} và Sự đối ngẫu RNN–Attention

Điều kỳ diệu nhất của công thức trên xuất hiện khi ta đặt hai biến trạng thái tích lũy theo thời gian tt:

Tại mỗi bước thời gian tt, trạng thái mới chỉ phụ thuộc vào trạng thái ở bước t−1t - 1 và token hiện tại (qt,kt,vt)(q_t, k_t, v_t):

St=St−1+ϕ(kt) vt⊤∈Rd×d,zt=zt−1+ϕ(kt)∈Rd,yt=ϕ(qt)⊤Stϕ(qt)⊤zt+ε∈R1×d.\begin{aligned} S_t &= S_{t-1} + \phi(k_t)\, v_t^\top \quad \in \mathbb{R}^{d \times d}, \\[4pt] z_t &= z_{t-1} + \phi(k_t) \quad \in \mathbb{R}^{d}, \\[4pt] y_t &= \frac{\phi(q_t)^\top S_t}{\phi(q_t)^\top z_t + \varepsilon} \quad \in \mathbb{R}^{1 \times d}. \end{aligned}

Hãy dừng lại một nhịp để thưởng thức hệ phương trình này! Đây chính là Sự đối ngẫu giữa Attention và RNN (Attention–RNN Duality):

  1. Khi huấn luyện (Chế độ Song song): Toàn bộ chuỗi đầu vào đã biết trước, ta tính song song trên GPU bằng các phép nhân ma trận khối (chunkwise / parallel scan) mà không cần vòng lặp tuần tự theo tt.
  2. Khi suy luận tự hồi quy (Chế độ Truy hồi — Recurrent mode): Thay vì phải lưu trữ KV Cache phình to vô hạn theo LL, ta chỉ cần giữ duy nhất một ma trận trạng thái cố định St∈Rd×dS_t \in \mathbb{R}^{d \times d} và một vector zt∈Rdz_t \in \mathbb{R}^d. Mỗi khi sinh thêm một token mới, chi phí cập nhật StS_t và truy xuất yty_t là hằng số O(d2)\mathcal{O}(d^2) — tức O(1)\mathcal{O}(1) theo chiều dài chuỗi LL!

Hướng tiếp cận biến RNN thành kiến trúc huấn luyện song song như Transformer cũng là nền tảng của dòng mô hình RWKV (Reinventing RNNs for the Transformer Era) (Peng & others, 2023), nơi tác giả bổ sung hệ số suy giảm theo thời gian e−we^{-w} vào trạng thái tích lũy để các token gần hiện tại có trọng số cao hơn các token ở xa.

Hãy khám phá trực quan hai cách đặt dấu ngoặc của Linear Attention trong biểu đồ tương tác dưới đây:

Sự đối ngẫu giữa Dạng Song song và Dạng Truy hồi trong Linear Attention
Chuyển đổi giữa hai cách kết hợp phép nhân ma trận: (φ(Q)φ(K)ᵀ)V tạo ra ma trận chú ý bậc hai L×L, trong khi φ(Q)(φ(K)ᵀV) nén lịch sử vào ma trận trạng thái cố định S_t kích thước d×d. Kéo thanh trượt L để xem mức tiết kiệm phép tính.

Trong bài tập dưới đây, bạn sẽ tự tay cài đặt Causal Linear Attention ở cả hai chế độ và chứng minh rằng chúng cho ra cùng một tensor đầu ra tới từng chữ số thập phân.

Bài tập 6.2 Chứng minh sự tương đương giữa Dạng Song song và Dạng Truy hồi của Linear Attention
Dữ liệu: Tổng hợp (PyTorch) · Thời gian chạy: < 1 phút

Với tensor đầu vào Q,K,V∈RB×L×dQ, K, V \in \mathbb{R}^{B \times L \times d} (B=4,L=256,d=32B=4, L=256, d=32) và hàm ánh xạ đặc trưng dương ϕ(x)=elu(x)+1\phi(x) = \text{elu}(x) + 1:

  1. Cài đặt hàm causal_linear_attention_parallel(Q, K, V) tính ma trận tương đồng ϕ(Q)ϕ(K)⊤\phi(Q)\phi(K)^\top, áp dụng mặt nạ nhân quả tam giác dưới (torch.tril), nhân với VV và chia chuẩn hóa theo tổng từng hàng.
  2. Cài đặt hàm causal_linear_attention_recurrent(Q, K, V) duyệt qua từng bước t=0…L−1t = 0 \dots L-1, cập nhật ma trận trạng thái St=St−1+ϕ(kt)vt⊤∈Rd×dS_t = S_{t-1} + \phi(k_t) v_t^\top \in \mathbb{R}^{d \times d} cùng vector chuẩn hóa zt=zt−1+ϕ(kt)∈Rdz_t = z_{t-1} + \phi(k_t) \in \mathbb{R}^d, rồi tính yty_t.
  3. Đo sai số tuyệt đối cực đại max⁡∣Yparallel−Yrecurrent∣\max |Y_{\text{parallel}} - Y_{\text{recurrent}}| và kiểm tra xem nó có nhỏ hơn 10−610^{-6} hay không.
Mã khởi tạo
import torch
import torch.nn.functional as F

torch.manual_seed(0)
B, L, d = 4, 256, 32

# Khởi tạo Q, K, V ngẫu nhiên và hàm ánh xạ đặc trưng dương phi(x)
Q = torch.randn(B, L, d)
K = torch.randn(B, L, d)
V = torch.randn(B, L, d)

def phi(x):
    return F.elu(x) + 1.0

print(f"Q, K, V shape: {tuple(Q.shape)}")
Q, K, V shape: (4, 256, 32)
Hint

Ở dạng song song, sau khi tính scores = torch.matmul(Qp, Kp.transpose(-1, -2)) kích thước (B, L, L), tạo mặt nạ mask = torch.tril(torch.ones(L, L, dtype=torch.bool)) và xóa phần tam giác trên bằng scores = scores.masked_fill(~mask, 0.0). Tử số là scores @ V, mẫu số là scores.sum(dim=-1, keepdim=True) + eps.

Hint

Ở dạng truy hồi, tại mỗi bước t, lấy k_t có kích thước (B, d, 1) và v_t có kích thước (B, 1, d). Tích ngoài ϕ(kt)vt⊤\phi(k_t) v_t^\top chính là torch.matmul(k_t, v_t) có kích thước (B, d, d). Tích của truy vấn q_t kích thước (B, 1, d) với trạng thái S kích thước (B, d, d) là torch.matmul(q_t, S).squeeze(1).

Điểm kiểm tra kết quả bạn cần đạt được

Hai hàm cài đặt theo hai thuật toán hoàn toàn khác nhau — một bên dùng ma trận chú ý 256×256256 \times 256, một bên chỉ cập nhật ma trận trạng thái cố định 32×3232 \times 32 — cho kết quả trùng khớp với sai số tuyệt đối cực đại chỉ là 4.77×10−74.77 \times 10^{-7} (do sai số làm tròn dấu phẩy động 32-bit).

Output shape: (4, 256, 32)
Trạng thái truy hồi cuối S_L shape: (4, 32, 32)
Sai số tuyệt đối cực đại |Y_parallel - Y_recurrent|: 4.77e-07
Kiểm chứng trùng khớp (< 1e-6): True
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import torch
import torch.nn.functional as F

torch.manual_seed(0)
B, L, d = 4, 256, 32

# Khởi tạo Q, K, V ngẫu nhiên và hàm ánh xạ đặc trưng dương phi(x)
Q = torch.randn(B, L, d)
K = torch.randn(B, L, d)
V = torch.randn(B, L, d)

def phi(x):
    return F.elu(x) + 1.0

print(f"Q, K, V shape: {tuple(Q.shape)}")
Q, K, V shape: (4, 256, 32)
# (1) Dạng ma trận song song (Parallel form) khi huấn luyện: O(L^2 d)
def causal_linear_attention_parallel(Q, K, V, eps=1e-6):
    Qp, Kp = phi(Q), phi(K)                        # (B, L, d)
    scores = torch.matmul(Qp, Kp.transpose(-1, -2))  # (B, L, L)
    causal_mask = torch.tril(torch.ones(L, L, dtype=torch.bool))
    scores = scores.masked_fill(~causal_mask, 0.0)
    num = torch.matmul(scores, V)                  # (B, L, d)
    den = scores.sum(dim=-1, keepdim=True) + eps   # (B, L, 1)
    return num / den


# (2) Dạng truy hồi trạng thái (Recurrent form) khi suy luận: O(L d^2)
def causal_linear_attention_recurrent(Q, K, V, eps=1e-6):
    Qp, Kp = phi(Q), phi(K)
    S = torch.zeros(B, d, d)   # Ma trận trạng thái S_t in R^{d x d}
    z = torch.zeros(B, d, 1)   # Vector tổng khóa chuẩn hóa z_t in R^d
    outputs = []

    for t in range(L):
        q_t = Qp[:, t, :].unsqueeze(1)             # (B, 1, d)
        k_t = Kp[:, t, :].unsqueeze(-1)            # (B, d, 1)
        v_t = V[:, t, :].unsqueeze(1)              # (B, 1, d)

        S = S + torch.matmul(k_t, v_t)             # k_t * v_t^T
        z = z + k_t
        num_t = torch.matmul(q_t, S).squeeze(1)    # (B, d)
        den_t = torch.matmul(q_t, z).squeeze(1) + eps
        outputs.append(num_t / den_t)

    return torch.stack(outputs, dim=1), S


y_par = causal_linear_attention_parallel(Q, K, V)
y_rec, S_final = causal_linear_attention_recurrent(Q, K, V)
max_err = (y_par - y_rec).abs().max().item()

print(f"Output shape: {tuple(y_rec.shape)}")
print(f"Trạng thái truy hồi cuối S_L shape: {tuple(S_final.shape)}")
print(f"Sai số tuyệt đối cực đại |Y_parallel - Y_recurrent|: {max_err:.2e}")
print(f"Kiểm chứng trùng khớp (< 1e-6): {max_err < 1e-6}")
Output shape: (4, 256, 32)
Trạng thái truy hồi cuối S_L shape: (4, 32, 32)
Sai số tuyệt đối cực đại |Y_parallel - Y_recurrent|: 4.77e-07
Kiểm chứng trùng khớp (< 1e-6): True
Điều cần lưu ý

Bài tập này hé lộ một góc nhìn sâu sắc: KV Cache thực chất là một trạng thái mở rộng còn ma trận St∈Rd×dS_t \in \mathbb{R}^{d \times d} là một trạng thái nén có mất mát (lossy compression). Trong Linear Attention thuần túy, vì St=St−1+ϕ(kt)vt⊤S_t = S_{t-1} + \phi(k_t) v_t^\top chỉ cộng dồn mọi cặp khóa – giá trị với hệ số bằng 11 mà không bao giờ xóa đi thông tin cũ, sau hàng nghìn bước trạng thái StS_t sẽ bị “bão hòa” bởi nhiễu quá khứ. Để khắc phục điểm yếu này, ta cần một cơ chế biết chọn lọc thông tin cần nhớ và cần quên theo ngữ cảnh — đó chính là động lực dẫn tới Mamba (Selective SSM) ở mục tiếp theo!


3. Mô hình Không gian Trạng thái Chọn lọc (SSM & Mamba)

Trong lý thuyết điều khiển học và xử lý tín hiệu, Mô hình Không gian Trạng thái (State Space Model — SSM) mô tả cách một tín hiệu đầu vào một chiều x(t)∈Rx(t) \in \mathbb{R} điều khiển một vector trạng thái ẩn h(t)∈RNh(t) \in \mathbb{R}^N và sinh ra tín hiệu đầu ra y(t)∈Ry(t) \in \mathbb{R} thông qua hệ phương trình vi phân tuyến tính bậc nhất:

h′(t)=A h(t)+B x(t),y(t)=C h(t),\begin{aligned} h'(t) &= A\, h(t) + B\, x(t), \\[4pt] y(t) &= C\, h(t), \end{aligned}

trong đó A∈RN×NA \in \mathbb{R}^{N \times N} là ma trận chuyển trạng thái (quyết định tốc độ suy giảm hoặc dao động của bộ nhớ), B∈RN×1B \in \mathbb{R}^{N \times 1} là ma trận chiếu đầu vào, và C∈R1×NC \in \mathbb{R}^{1 \times N} là ma trận đọc trạng thái ra đầu ra. (Khi áp dụng cho vector embedding DD chiều của LLM, ta chạy độc lập một hệ SSM như vậy trên từng kênh trong DD kênh).

3.1 Rời rạc hóa Zero-Order Hold (ZOH) và Mô hình S4

Vì văn bản là một chuỗi các token rời rạc x1,x2,…,xLx_1, x_2, \dots, x_L thay vì hàm liên tục x(t)x(t), ta cần rời rạc hóa (discretize) hệ phương trình vi phân trên bằng một tham số bước nhảy thời gian (step size) Δ>0\Delta > 0. Theo quy tắc giữ bậc không (Zero-Order Hold — ZOH), các ma trận rời rạc (Aˉ,Bˉ)(\bar{A}, \bar{B}) được tính từ (A,B,Δ)(A, B, \Delta) như sau:

Aˉ=exp⁡(ΔA),Bˉ=(ΔA)−1(exp⁡(ΔA)−I)⋅ΔB≈ΔB,\bar{A} = \exp(\Delta A), \qquad \bar{B} = (\Delta A)^{-1}\big(\exp(\Delta A) - I\big) \cdot \Delta B \approx \Delta B,

trong đó xấp xỉ Bˉ≈ΔB\bar{B} \approx \Delta B là khai triển Euler bậc nhất cực kỳ trực quan. Khi đó, hệ vi phân trở thành một mạng RNN tuyến tính theo thời gian rời rạc tt:

ht=Aˉ ht−1+Bˉ xt,yt=C ht.h_t = \bar{A}\, h_{t-1} + \bar{B}\, x_t, \qquad y_t = C\, h_t.

Năm 2022, Albert Gu, Karan Goel và Christopher Ré giới thiệu mô hình S4 (Structured State Spaces for Sequence Modeling) (Gu & Ré, 2022). Họ nhận ra rằng nếu các ma trận (A,B,C,Δ)(A, B, C, \Delta) là bất biến theo thời gian (Linear Time-Invariant — LTI, tức không phụ thuộc vào bước tt hay nội dung xtx_t), thì việc mở rộng đệ quy của hth_t sẽ biến toàn bộ chuỗi đầu ra yy thành một phép tích chập toàn cục (global convolution):

yt=CBˉxt+CAˉBˉxt−1+⋯+CAˉt−1Bˉx1⟺y=x∗K‾,K‾=(CBˉ, CAˉBˉ, …, CAˉL−1Bˉ).y_t = C \bar{B} x_t + C \bar{A} \bar{B} x_{t-1} + \dots + C \bar{A}^{t-1} \bar{B} x_1 \quad \Longleftrightarrow \quad y = x * \overline{K}, \quad \overline{K} = \big(C\bar{B},\, C\bar{A}\bar{B},\, \dots,\, C\bar{A}^{L-1}\bar{B}\big).

Nhờ định lý tích chập và cấu trúc ma trận đường chéo HiPPO của AA, S4 có thể tính toàn bộ chuỗi dài LL song song bằng biến đổi Fourier nhanh (FFT) trong thời gian O(Llog⁡L)\mathcal{O}(L \log L) khi huấn luyện, và chạy như RNN trong thời gian O(1)\mathcal{O}(1) khi suy luận!

3.2 Đột phá của Mamba: Cơ chế Quét Chọn lọc (Selective Scan)

S4 đạt kết quả xuất sắc trên các chuỗi tín hiệu vật lý (âm thanh, sóng điện tâm đồ), nhưng lại thua kém Transformer trên mô hình hóa ngôn ngữ tự nhiên. Tại sao?

Trong bài báo Mamba (Gu & Dao, 2023), Albert Gu và Tri Dao đã chỉ ra căn nguyên cốt lõi: ngôn ngữ tự nhiên đòi hỏi sự nén thông tin phụ thuộc vào ngữ cảnh (content-aware reasoning), điều mà mọi hệ thống bất biến thời gian (LTI) đều bất lực.

Hãy tưởng tượng mô hình đọc câu: “Mã xác thực của bạn là A7 … uhm … à … K9 … ờm … M2” và phải nhắc lại mã A7-K9-M2 ở cuối câu (bài toán Selective Copying).

Bt=WB xt,Ct=WC xt,Δt=softplus(WΔ xt+bΔ),B_t = W_B\, x_t, \qquad C_t = W_C\, x_t, \qquad \Delta_t = \text{softplus}\big(W_\Delta\, x_t + b_\Delta\big),

trong đó softplus(z)=ln⁡(1+ez)>0\text{softplus}(z) = \ln(1 + e^z) > 0 đảm bảo bước nhảy thời gian luôn dương, còn ma trận AA được chọn là ma trận đường chéo âm A=−exp⁡(log_A)<0A = -\exp(\text{log\_A}) < 0 để hệ thống luôn ổn định. Khi đó, ở mỗi bước tt:

Aˉt=exp⁡(ΔtA)∈(0,1)N,Bˉt=ΔtBt,ht=Aˉt⊙ht−1+Bˉt⊙xt.\bar{A}_t = \exp(\Delta_t A) \in (0, 1)^N, \qquad \bar{B}_t = \Delta_t B_t, \qquad h_t = \bar{A}_t \odot h_{t-1} + \bar{B}_t \odot x_t.

3.3 Đối ngẫu Không gian Trạng thái có Cấu trúc (SSD) trong Mamba-2

Năm 2024, Tri Dao và Albert Gu tiếp tục công bố Mamba-2 (Dao & Gu, 2024) với lý thuyết Đối ngẫu Không gian Trạng thái có Cấu trúc (Structured State Space Duality — SSD). Họ chứng minh rằng khi ma trận chuyển trạng thái Aˉt=atI\bar{A}_t = a_t I là tích của một vô hướng at∈(0,1)a_t \in (0, 1) với ma trận đơn vị, phương trình SSM của Mamba và phương trình Linear Attention ở Mục 2 trở thành một thể thống nhất:

yt=∑j=1t(∏s=j+1tas)(ct⊤bj) xj⟺Y=(Ldecay⊙(CB⊤))X,y_t = \sum_{j=1}^{t} \left(\prod_{s=j+1}^{t} a_s\right) (c_t^\top b_j)\, x_j \quad \Longleftrightarrow \quad Y = \big(L_{\text{decay}} \odot (C B^\top)\big) X,

trong đó C,B,XC, B, X tương ứng trực tiếp với Q,K,VQ, K, V của Attention, còn LdecayL_{\text{decay}} là ma trận mặt nạ nhân quả bán tách được (1-semiseparable matrix) chứa tích các hệ số suy giảm chọn lọc asa_s. Nhờ chia chuỗi thành các khối nhỏ (chunkwise decomposition), Mamba-2 tận dụng trực tiếp các nhân nhân ma trận Tensor Cores của GPU, giúp huấn luyện nhanh hơn Mamba-1 từ 2 đến 8 lần trong khi cho phép tăng kích thước trạng thái NN lên gấp 8 lần (N=128N = 128).

Hãy tương tác với biểu đồ dưới đây để thấy rõ cách cổng chọn lọc Δt(xt)\Delta_t(x_t) của Mamba giải quyết bài toán Sao chép Chọn lọc mà SSM bất biến thời gian (LTI) bó tay:

Cơ chế Chọn lọc (Selective Scan) của Mamba trên Bài toán Sao chép Chọn lọc
So sánh Mamba (Selective SSM) và LTI SSM (S4) khi đọc chuỗi chứa các mã quan trọng (A7, K9, M2) xen kẽ bởi các token nhiễu. Bạn có thể nhấp trực tiếp vào từng token để chuyển đổi giữa Tín hiệu và Nhiễu.

Bây giờ, hãy tự tay lập trình cả hai mô hình (LTI SSM và Selective SSM) trên bài toán Sao chép Chọn lọc trong PyTorch!

Bài tập 6.3 Cài đặt Selective SSM (Mamba) và Giải bài toán Nhớ có Chọn lọc
Dữ liệu: Selective Copying · Thời gian chạy: < 1 phút

Xét bài toán Nhớ có Chọn lọc (Selective Copying): mỗi chuỗi gồm L=24L = 24 bước, tại mỗi bước tt đầu vào là cặp (vt,mt)∈R2(v_t, m_t) \in \mathbb{R}^2 gồm giá trị ngẫu nhiên vt∼N(0,1)v_t \sim \mathcal{N}(0, 1) và cờ đánh dấu mt∈{0,1}m_t \in \{0, 1\}. Trong cả chuỗi chỉ có duy nhất một bước ngẫu nhiên t∗t^* có mt∗=1m_{t^*} = 1 (token tín hiệu), các bước còn lại đều có mt=0m_t = 0 (token nhiễu). Ở bước cuối cùng t=Lt = L, mô hình phải dự đoán lại chính xác giá trị vt∗v_{t^*}.

  1. Cài đặt lớp SimpleSSM với hai chế độ:
    • selective=False (LTI SSM): Δ,B,C\Delta, B, C là các vector tham số cố định không phụ thuộc vào xtx_t.
    • selective=True (Mamba): Δt=softplus(WΔxt)\Delta_t = \text{softplus}(W_\Delta x_t), Bt=WBxtB_t = W_B x_t, Ct=WCxtC_t = W_C x_t.
  2. Huấn luyện cả hai mô hình trong 250 bước bằng Adam và so sánh sai số bình phương trung bình (MSE) cuối cùng.
  3. Kiểm tra giá trị trung bình của bước nhảy Δt\Delta_t mà mô hình Mamba đã học được tại vị trí tín hiệu (mt=1m_t = 1) so với tại vị trí nhiễu (mt=0m_t = 0).
Mã khởi tạo
import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(7)

# Bài toán Nhớ có Chọn lọc (Selective Copying):
# Chuỗi gồm L=24 bước; chỉ có 1 token tín hiệu (marker=1) ở vị trí ngẫu nhiên,
# các bước còn lại là nhiễu (marker=0). Mô hình phải xuất lại giá trị tín hiệu ở bước cuối.
def sample_selective_copy_batch(batch_size=64, seq_len=24):
    values = torch.randn(batch_size, seq_len, 1)
    markers = torch.zeros(batch_size, seq_len, 1)
    pos = torch.randint(0, seq_len - 4, (batch_size,))
    for b in range(batch_size):
        markers[b, pos[b], 0] = 1.0
    x = torch.cat([values, markers], dim=-1)      # (B, L, 2)
    y = values[torch.arange(batch_size), pos, 0]  # (B,)
    return x, y, pos

x_demo, y_demo, pos_demo = sample_selective_copy_batch(4, 12)
print(f"Batch x shape: {tuple(x_demo.shape)}, target y shape: {tuple(y_demo.shape)}")
Batch x shape: (4, 12, 2), target y shape: (4,)
Hint

Đảm bảo ma trận đường chéo AA luôn âm bằng cách tham số hóa A = -torch.exp(self.log_A). Tại mỗi bước thời gian t, hệ số giữ trạng thái rời rạc là a_bar = torch.exp(delta[:, t, :] * A) và hệ số ghi đầu vào là b_bar = delta[:, t, :] * B_t[:, t, :].

Hint

Cập nhật trạng thái ẩn h = a_bar * h + b_bar * u[:, t, :] trong vòng lặp for t in range(L):. Ở bước cuối cùng, chiếu trạng thái ra dự đoán vô hướng bằng self.out_head(h * C_t[:, -1, :]).squeeze(-1).

Điểm kiểm tra kết quả bạn cần đạt được

Mô hình LTI SSM thất bại hoàn toàn với sai số MSE giữ ở mức 0.84210.8421 (gần như đoán mò giá trị trung bình 00), vì vị trí t∗t^* của token tín hiệu thay đổi ngẫu nhiên ở mỗi mẫu dữ liệu. Ngược lại, mô hình Selective SSM (Mamba) hội tụ về MSE =0.0014= 0.0014! Khi kiểm tra giá trị Δt\Delta_t, ta thấy Mamba đã tự học được quy tắc: mở rộng Δt≈2.841\Delta_t \approx 2.841 khi gặp cờ mt=1m_t = 1 để ghi tín hiệu vào bộ nhớ, và khóa chặt Δt≈0.018\Delta_t \approx 0.018 khi gặp mt=0m_t = 0 để đóng băng trạng thái ẩn.

[SSM Bất biến thời gian (LTI)] MSE cuối: 0.8421
[Selective SSM (Mamba)]        MSE cuối: 0.0014

Kiểm tra cổng chọn lọc Delta_t của Mamba:
  Trung bình Delta_t tại token TÍN HIỆU (marker=1): 2.841
  Trung bình Delta_t tại token NHIỄU    (marker=0): 0.018
Lời giải 2 ô mã

Chạy các ô mã theo đúng thứ tự này trong một notebook mới: chúng đã bao gồm các lệnh import, bước tải dữ liệu và khởi tạo hạt giống ngẫu nhiên (random seed).

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(7)

# Bài toán Nhớ có Chọn lọc (Selective Copying):
# Chuỗi gồm L=24 bước; chỉ có 1 token tín hiệu (marker=1) ở vị trí ngẫu nhiên,
# các bước còn lại là nhiễu (marker=0). Mô hình phải xuất lại giá trị tín hiệu ở bước cuối.
def sample_selective_copy_batch(batch_size=64, seq_len=24):
    values = torch.randn(batch_size, seq_len, 1)
    markers = torch.zeros(batch_size, seq_len, 1)
    pos = torch.randint(0, seq_len - 4, (batch_size,))
    for b in range(batch_size):
        markers[b, pos[b], 0] = 1.0
    x = torch.cat([values, markers], dim=-1)      # (B, L, 2)
    y = values[torch.arange(batch_size), pos, 0]  # (B,)
    return x, y, pos

x_demo, y_demo, pos_demo = sample_selective_copy_batch(4, 12)
print(f"Batch x shape: {tuple(x_demo.shape)}, target y shape: {tuple(y_demo.shape)}")
Batch x shape: (4, 12, 2), target y shape: (4,)
class SimpleSSM(nn.Module):
    def __init__(self, d_in=2, d_state=16, selective=True):
        super().__init__()
        self.selective = selective
        self.d_state = d_state
        self.in_proj = nn.Linear(d_in, d_state)
        self.log_A = nn.Parameter(torch.zeros(d_state))

        if selective:
            # Mamba: Delta_t, B_t, C_t là hàm tuyến tính của đầu vào x_t
            self.proj_delta = nn.Linear(d_in, d_state)
            self.proj_B = nn.Linear(d_in, d_state)
            self.proj_C = nn.Linear(d_in, d_state)
        else:
            # LTI SSM: Delta, B, C cố định, bất biến theo thời gian
            self.raw_delta = nn.Parameter(torch.full((d_state,), -1.0))
            self.B = nn.Parameter(torch.randn(d_state) * 0.2)
            self.C = nn.Parameter(torch.randn(d_state) * 0.2)

        self.out_head = nn.Linear(d_state, 1)

    def forward(self, x):
        B_sz, L, _ = x.shape
        u = self.in_proj(x)                        # (B, L, N)
        A = -torch.exp(self.log_A)                 # (N,) luôn âm để ổn định

        if self.selective:
            delta = F.softplus(self.proj_delta(x)) # (B, L, N)
            B_t = self.proj_B(x)
            C_t = self.proj_C(x)
        else:
            delta = F.softplus(self.raw_delta).view(1, 1, -1).expand(B_sz, L, -1)
            B_t = self.B.view(1, 1, -1).expand(B_sz, L, -1)
            C_t = self.C.view(1, 1, -1).expand(B_sz, L, -1)

        # Rời rạc hóa và quét trạng thái h_t = A_bar * h_{t-1} + B_bar * u_t
        h = torch.zeros(B_sz, self.d_state)
        for t in range(L):
            a_bar = torch.exp(delta[:, t, :] * A)
            b_bar = delta[:, t, :] * B_t[:, t, :]
            h = a_bar * h + b_bar * u[:, t, :]

        y_hat = self.out_head(h * C_t[:, -1, :]).squeeze(-1)
        return y_hat, delta


def train_ssm(selective):
    torch.manual_seed(7)
    model = SimpleSSM(selective=selective)
    opt = torch.optim.Adam(model.parameters(), lr=1e-2)
    for _ in range(250):
        bx, by, _ = sample_selective_copy_batch(64, 24)
        pred, _ = model(bx)
        loss = F.mse_loss(pred, by)
        opt.zero_grad()
        loss.backward()
        opt.step()
    return model, loss.item()


lti_model, lti_mse = train_ssm(selective=False)
sel_model, sel_mse = train_ssm(selective=True)
print(f"[SSM Bất biến thời gian (LTI)] MSE cuối: {lti_mse:.4f}")
print(f"[Selective SSM (Mamba)]        MSE cuối: {sel_mse:.4f}\n")

with torch.no_grad():
    bx, _, _ = sample_selective_copy_batch(128, 24)
    _, delta = sel_model(bx)
    d_mean = delta.mean(dim=-1)
    mask = bx[:, :, 1] == 1.0
    print("Kiểm tra cổng chọn lọc Delta_t của Mamba:")
    print(f"  Trung bình Delta_t tại token TÍN HIỆU (marker=1): {d_mean[mask].mean():.3f}")
    print(f"  Trung bình Delta_t tại token NHIỄU    (marker=0): {d_mean[~mask].mean():.3f}")
[SSM Bất biến thời gian (LTI)] MSE cuối: 0.8421
[Selective SSM (Mamba)]        MSE cuối: 0.0014

Kiểm tra cổng chọn lọc Delta_t của Mamba:
  Trung bình Delta_t tại token TÍN HIỆU (marker=1): 2.841
  Trung bình Delta_t tại token NHIỄU    (marker=0): 0.018
Điều cần lưu ý

Kết quả thực nghiệm của bạn tái hiện chính xác luận điểm trung tâm của bài báo Mamba (Gu & Dao, 2023): một hệ thống bất biến thời gian (dù dùng tích chập toàn cục lớn đến đâu) chỉ có thể học các mẫu phụ thuộc vào khoảng cách vị trí cố định, nên khi token quan trọng xuất hiện ở vị trí ngẫu nhiên, nó hoàn toàn bất lực. Chỉ bằng cách làm cho bước nhảy rời rạc hóa Δt\Delta_t phụ thuộc vào đầu vào xtx_t, Mamba biến một phương trình vi phân tuyến tính thành một bộ nhớ chọn lọc theo nội dung với chi phí suy luận O(1)\mathcal{O}(1).


4. Kiến trúc Lai (Hybrid SSM-Transformer) và Mô hình Ngôn ngữ Khuếch tán (Diffusion LMs)

Nếu Mamba vừa có độ phức tạp tuyến tính O(L)\mathcal{O}(L) vừa có khả năng nhớ chọn lọc, liệu nó đã có thể thay thế hoàn toàn Softmax Attention trong mọi tác vụ hay chưa? Câu trả lời thực tế là: chưa hoàn toàn, nhưng sự kết hợp giữa cả hai lại tạo ra kiến trúc mạnh nhất hiện nay.

4.1 Điểm mù “Sao chép nguyên văn” của SSM và Kiến trúc Lai Jamba

Hãy hình dung bạn đưa cho mô hình một tài liệu dài 50 trang chứa hàng trăm mã số hợp đồng khác nhau, rồi ở dòng cuối cùng mới hỏi: “Hãy trích xuất nguyên văn số điện thoại nằm ngay sau mã hợp đồng #8492 ở trang 12”.

Khám phá này đã dẫn đến xu hướng thiết kế Kiến trúc Lai (Hybrid SSM–Transformer), tiêu biểu là mô hình Jamba của AI21 Labs (Lieber & others, 2024) (và sau đó là Zamba, Samba, Bamba, Nemotron-H). Thay vì dùng 100% lớp Attention hoặc 100% lớp Mamba, Jamba xếp chồng các khối theo tỷ lệ 7 lớp Mamba : 1 lớp Attention, đồng thời tích hợp Sparse MoE vào các lớp FFN!

4.2 Vượt khỏi sinh tuần tự: Mô hình Ngôn ngữ Khuếch tán Mặt nạ (Masked Diffusion LMs)

Cả Transformer, MoE lẫn Mamba đều chia sẻ chung một giả định nền tảng từ Chương 0 đến nay: sinh văn bản tự hồi quy từ trái sang phải (left-to-right autoregressive generation) P(x1,…,xL)=∏t=1LP(xt∣x<t)P(x_1, \dots, x_L) = \prod_{t=1}^{L} P(x_t \mid x_{<t}). Cách sinh từng token một này khiến độ trễ suy luận luôn tỉ lệ thuận với số token đầu ra và khó sửa lại các token đã lỡ sinh ở đầu câu.

Liệu ta có thể sinh văn bản theo cách các mô hình Diffusion (như Stable Diffusion, Flux) tạo ra bức ảnh — tức khởi tạo toàn bộ câu bằng các ô trống rồi tinh chỉnh song song toàn bộ câu qua vài chục bước khử nhiễu?

Đó chính là ý tưởng của Mô hình Ngôn ngữ Khuếch tán Mặt nạ (Masked Diffusion Language Models — MDLM) (Sahoo & others, 2024) (cùng các biến thể quy mô lớn như LLaDA hay Mercury):

  1. Quá trình thuận (Làm nhiễu — Forward Masking Process): Xuất phát từ câu văn sạch x0x_0 độ dài LL, ở thời điểm t∈[0,1]t \in [0, 1], mỗi token độc lập có xác suất αt=1−t\alpha_t = 1 - t được giữ nguyên và xác suất tt bị thay thế bằng token mặt nạ [MASK]. Khi t=1t = 1, toàn bộ câu chỉ gồm toàn [MASK].
  2. Quá trình ngược (Khử nhiễu song song — Reverse Unmasking Process): Một mạng Transformer hai chiều (không dùng mặt nạ nhân quả, giống như BERT ở Chương 0 Mục 10.2!) đọc chuỗi đang bị che một phần xtx_t và dự đoán phân phối từ vựng gốc cho tất cả các vị trí [MASK] cùng một lúc. Ở mỗi bước khử nhiễu từ tt về s<ts < t, một tỷ lệ các vị trí [MASK] có độ tự tin cao nhất được “lật mở” đồng thời thành các từ cụ thể, cho đến khi cả câu hoàn chỉnh tại t=0t = 0.

Như Sahoo et al. (Sahoo & others, 2024) chứng minh, hàm mục tiêu huấn luyện liên tục theo thời gian của MDLM thực chất là trung bình có trọng số của các hàm mất mát Masked Language Modeling (BERT) ở mọi tỷ lệ che mặt nạ t∈(0,1]t \in (0, 1]:

LMDLM=Et∼U(0,1)[1t∑ℓ:xt(ℓ)=[MASK]−log⁡pθ ⁣(x0(ℓ) | xt)].\mathcal{L}_{\text{MDLM}} = \mathbb{E}_{t \sim \mathcal{U}(0, 1)} \left[ \frac{1}{t} \sum_{\ell : x_t^{(\ell)} = \text{[MASK]}} -\log p_\theta\!\left(x_0^{(\ell)} \,\middle|\, x_t\right) \right].

Bằng cách cho phép lật mở hàng chục token trong một lần chạy mạng duy nhất, các mô hình khuếch tán rời rạc mở ra hướng đi mới cho việc sinh mã nguồn và chỉnh sửa văn bản song song với tốc độ vượt xa giải mã tự hồi quy truyền thống.

4.3 Bảng tổng kết các Kiến trúc Mô hình Ngôn ngữ

Để khép lại Chương 6, bảng dưới đây đặt cạnh nhau 5 họ kiến trúc mà ta đã khảo sát để bạn dễ dàng đối chiếu sự đánh đổi về tính toán, bộ nhớ và năng lực biểu diễn:

Kiến trúcMô hình tiêu biểuFLOPs Tiền xử lý (Prefill)Bộ nhớ Trạng thái / KV CacheChi phí mỗi bước DecodeĐiểm mạnh & Đánh đổi cốt lõi
Dense TransformerLlama-3, Qwen-2.5O(L2d+Ld2)\mathcal{O}(L^2 d + L d^2)O(Ld)\mathcal{O}(L d)O(Ld+d2)\mathcal{O}(L d + d^2)Tra cứu nguyên văn trong ngữ cảnh hoàn hảo, nhưng nghẽn bậc hai O(L2)\mathcal{O}(L^2) và tốn KV Cache lớn khi LL dài.
Sparse MoE TransformerMixtral 8x7B, DeepSeek-V3O(L2d+kELDmoe)\mathcal{O}(L^2 d + \frac{k}{E} L D_{\text{moe}})O(Ld)\mathcal{O}(L d)O(Ld+kEDmoe)\mathcal{O}(L d + \frac{k}{E} D_{\text{moe}})Dung lượng tham số khổng lồ với chi phí FLOPs nhỏ; cần VRAM lớn để chứa đủ EE chuyên gia và cần cân bằng tải.
Linear Attention / RWKVLinear Transformer, RWKV-6O(Ld2)\mathcal{O}(L d^2)O(d2)\mathcal{O}(d^2) (Hằng số)O(d2)\mathcal{O}(d^2) (Hằng số)Suy luận O(1)\mathcal{O}(1) không cần KV Cache; nhưng bộ nhớ ma trận cộng dồn cố định kém linh hoạt hơn cổng chọn lọc.
Selective SSM (Mamba)Mamba, Mamba-2O(LdN)\mathcal{O}(L d N)O(dN)\mathcal{O}(d N) (Hằng số)O(dN)\mathcal{O}(d N) (Hằng số)Cổng Δt(xt)\Delta_t(x_t) lọc nhiễu và nén ngữ cảnh dài cực hiệu quả; kém hơn Softmax Attention khi cần sao chép nguyên văn.
Hybrid SSM + Attention + MoEJamba, Zamba2O(1ML2d+LdN)\mathcal{O}(\frac{1}{M} L^2 d + L d N)O(1MLd+dN)\mathcal{O}(\frac{1}{M} L d + d N)O(1MLd+dN)\mathcal{O}(\frac{1}{M} L d + d N)Kết hợp tốc độ tuyến tính & tiết kiệm 85–90%85\text{–}90\% KV Cache của Mamba với khả năng tra cứu chính xác của Attention.
Đọc thêm Tại sao SSM gặp khó khăn với bài toán Sao chép Nguyên văn (Jelassi et al. 2024)?

Một định lý thú vị của Jelassi et al. (2024) (“Repeat After Me: Transformers are Better than State Space Models at Copying”) đã định lượng hóa chính xác sự khác biệt giữa Softmax Attention và SSM trên bài toán sao chép một chuỗi ngẫu nhiên độ dài KK bit:

  • Một mô hình SSM / Mamba có trạng thái ẩn cố định gồm BstateB_{\text{state}} bit thông tin sẽ không bao giờ sao chép chính xác được một chuỗi ngẫu nhiên có entropy lớn hơn BstateB_{\text{state}} bit, bất kể được huấn luyện lâu đến đâu (theo nguyên lý chuồng bồ câu của lý thuyết thông tin Shannon).
  • Một mô hình Transformer chỉ cần 2 tầng (tạo thành mạch Induction Head đã học ở Chương 2) có thể sao chép chuỗi có độ dài bất kỳ K≤LK \le L với số lượng tham số cố định, bởi vì KV Cache của nó tự động mở rộng dung lượng bộ nhớ tỷ lệ thuận với độ dài chuỗi đầu vào LL!

Đó là lý do vì sao việc giữ lại chỉ 1 tầng Softmax Attention trên mỗi 7–8 tầng Mamba (như trong Jamba) lại hiệu quả đến vậy: các tầng Mamba đảm nhận việc hiểu ngôn ngữ và tổng hợp ngữ nghĩa với chi phí O(L)\mathcal{O}(L), trong khi một vài tầng Attention đóng vai trò là “bộ nhớ truy cập ngẫu nhiên (RAM)” để tra cứu nguyên văn khi cần thiết.

Nếu muốn đào sâu hơn nữa, bạn có thể thử sức với các bài tập mở rộng dưới đây:

Mở rộng

Cân bằng tải không cần hàm phạt phụ trợ (DeepSeek-V3 Auxiliary-Loss-Free Balancing). Trong Bài tập 6.1, thay vì cộng αLaux\alpha \mathcal{L}_{\text{aux}} vào hàm mất mát, hãy cài đặt cơ chế của DeepSeek-V3 (DeepSeek-AI, 2024): thêm một vector độ lệch không nhận gradient b = torch.zeros(E) vào điểm định tuyến khi chọn Top-kk (scores_for_topk = probs + b), nhưng vẫn lấy trọng số tổ hợp weights từ probs gốc. Sau mỗi bước huấn luyện, cập nhật b[i] += gamma * (1/E - f_i). Kiểm chứng rằng tải của 8 chuyên gia vẫn cân bằng hoàn hảo mà hàm mất mát chính LMSE\mathcal{L}_{\text{MSE}} còn thấp hơn cả khi dùng Laux\mathcal{L}_{\text{aux}}!

Mở rộng

Hệ số suy giảm trong Linear Attention (RetNet / RWKV). Sửa lại hàm causal_linear_attention_recurrent và causal_linear_attention_parallel trong Bài tập 6.2 bằng cách thêm một hệ số suy giảm γ=0.95\gamma = 0.95: ở dạng truy hồi St=γSt−1+ϕ(kt)vt⊤S_t = \gamma S_{t-1} + \phi(k_t) v_t^\top, và ở dạng song song nhân ma trận chú ý với ma trận mặt nạ suy giảm Dt,j=γt−jD_{t, j} = \gamma^{t - j} (với t≥jt \ge j). Kiểm chứng rằng hai dạng vẫn trùng khớp với sai số <10−6< 10^{-6}.

Mở rộng

Lắp ráp khối Hybrid Jamba thu nhỏ. Kết hợp lớp CausalSelfAttention (Chương 2), lớp SimpleSSM (Bài tập 6.3) và lớp SparseMoELayer (Bài tập 6.1) để tạo thành một mô hình Hybrid 4 tầng (3 tầng SSM + 1 tầng Attention, sử dụng MoE thay cho FFN). So sánh tổng kích thước bộ nhớ trạng thái cần lưu trữ khi suy luận ở L=4096L = 4096 so với một mô hình Transformer 4 tầng thuần túy.


Tài liệu tham khảo

  1. 2017 Shazeer et al. Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. ICLR. 1
  2. 2020 Katharopoulos et al. Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention. ICML. 2
  3. 2022 Fedus, Zoph & Shazeer. Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity. JMLR. 1
  4. 2022 Gu, Goel & Ré. Efficiently Modeling Long Sequences with Structured State Spaces. ICLR. 3
  5. 2023 Gu & Dao. Mamba: Linear-Time Sequence Modeling with Selective State Spaces. COLM 2024 (arXiv 2023). 3
  6. 2023 Peng et al. RWKV: Reinventing RNNs for the Transformer Era. EMNLP Findings. 2
  7. 2024 Dao & Gu. Transformers are SSMs: Generalized Models and Efficient Algorithms Through Structured State Space Duality. ICML. 3
  8. 2024 DeepSeek-AI. DeepSeek-V3 Technical Report. arXiv. 1
  9. 2024 Jiang et al. Mixtral of Experts. arXiv. 1
  10. 2024 Lieber et al. Jamba: A Hybrid Transformer-Mamba Language Model. arXiv. 4
  11. 2024 Sahoo et al. Simple and Effective Masked Diffusion Language Models. NeurIPS. 4