跳至主要內容

    Transformer:位置、遮罩、殘差與平行計算

    位置編碼告訴模型順序,causal mask 限制它能看見的未來

    sunny 桑尼· · 39 MIN READ · 170 次瀏覽

    Transformer:位置、遮罩、殘差與平行計算
    本文目錄

    Transformer 有一個看似矛盾的特性:它可以在訓練時同時計算一整排 token,生成文字時卻又得一個 token 接一個 token。要理解這件事,先看三個設計如何分工:位置編碼提供順序線索;causal mask 限制每個位置可讀取的內容;attention、前饋網路與殘差區塊則逐層混合和整理表示。

    本文先用一組四維向量手算正弦位置編碼,再從完整 encoder-decoder 圖認識 block。接著釐清遮罩和 cross-attention 的工作,最後比較 teacher forcing 的平行計算、推論時的逐步生成,以及 post-norm 和 pre-norm。文末程式是一個手設參數的 CPU 機制例,不是訓練好的翻譯模型,也不是效能測試。

    位置編碼:讓表示帶上順序

    Self-attention 可以讓一個位置直接讀取多個其他位置,但「能彼此連接」本身沒有告訴模型誰先誰後。Transformer 的一種做法,是把位置向量加到 token embedding 上。兩者維度必須相同,因此相加後仍是同一個 d_model 寬度:

    輸入表示 = token embedding × √d_model + positional encoding
    

    原始 Transformer 論文使用固定的正弦與餘弦函數。對位置 pos、模型寬度 d_model,每一對特徵的角度如下:

    PE(pos, 2i)     = sin(pos / 10000^(2i / d_model))
    PE(pos, 2i + 1) = cos(pos / 10000^(2i / d_model))
    

    偶數欄放 sin,緊接的奇數欄放同頻率的 cos;i 遞增時,分母改變,各組函數的變化速度也不同。位置 0 時角度是 0,所以每對值都是 [sin(0), cos(0)] = [0, 1]。

    圖 1|橫軸是位置,圖例 Col 6–9 是位置編碼的四個欄位;不同欄位以不同頻率變化。這是 D2L PyTorch 教材範例程式輸出的曲線,不是訓練 loss 或詞義。來源:D2L〈Self-Attention and Positional Encoding〉,Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola,CC BY-SA 4.0。本文把教材 live 輸出 SVG 轉為白底 PNG,保留原曲線、座標、圖例與數值;改作圖同採 CC BY-SA 4.0。

    同一份位置編碼也可以改用矩陣看:每列是一個位置,每欄是一個 encoding dimension,顏色表示該位置與欄位的 sin/cos 值。色條範圍是 −1 到 1,因此深淺不是「這個 token 對另一個 token 的注意力」;它還沒有經過 query-key 配對或 softmax。

    圖 2|完整的 60 個位置 × 32 個 encoding dimensions;row 是 position、column 是 encoding dimension,右側色條為 −1 到 1。它是 D2L 作者程式產生的位置編碼值,不是 attention 權重。來源與作者同圖 1。本文保留原 SVG 中的矩陣及色條 raster,核對兩者原始尺寸與 SHA 後轉成白底 PNG,座標、顏色與值未改;改作圖同採 CC BY-SA 4.0。

    以程式中的第一個 embedding 為例,d_model=4,所以縮放因子是 √4=2。位置 0 的 encoding 是 [0, 1, 0, 1]:

    embedding             = [0.20, −0.10, 0.30, 0.40]
    embedding × 2         = [0.40, −0.20, 0.60, 0.80]
    PE(position=0)         = [0.00,  1.00, 0.00, 1.00]
    相加後                 = [0.40,  0.80, 0.60, 1.80]
    

    這說明同一個 token 放在不同位置時,送入後續 attention 的向量會不同。表中的小數是本篇程式實際使用的 sin/cos 值,四捨五入到四位:

    位置 pos欄 0 sin(pos)欄 1 cos(pos)欄 2 sin(pos/100)欄 3 cos(pos/100)
    00.00001.00000.00001.0000
    10.84150.54030.01001.0000
    20.9093−0.41610.02000.9998
    30.1411−0.99000.03000.9996

    這是原始 Transformer 採用的其中一種位置表示,不代表所有 Transformer 都必須用固定正弦表。正弦值也沒有被「訓練成位置標籤」;它們是依位置索引和公式算出,與 token embedding 相加。

    從整體架構看每一層在做什麼

    位置資訊進入表示後,encoder 可在來源序列的位置間做 self-attention;decoder 先對已提供的目標 prefix 做 masked self-attention,再用 cross-attention 讀 encoder 輸出,接著各自透過 positionwise feed-forward network(FFN)。圖中的 Add & norm 是殘差相加後再做正規化,虛線框內的 × n 表示重複堆疊同類 block。頂端 FC 是輸出投影;這張圖沒有畫 softmax,因此不應把它說成已輸出的詞彙機率。

    圖 3|D2L 自有 Transformer 架構示意,包含來源/目標 embedding 加位置、encoder self-attention、decoder masked self-attention 與 cross-attention、FFN、Add & norm 及 FC 輸出。這是教材概念圖,不是 Vaswani 論文原圖,也不是本站跑出的模型。來源:D2L〈The Transformer Architecture〉,Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola,CC BY-SA 4.0。本文將固定版完整 SVG 轉成白底 PNG;手機版先放完整圖,再依原圖順序放大 encoder 與 decoder 區域,僅以白色間隔分開;跨區連線請回看全圖。改作圖同採 CC BY-SA 4.0。

    一個常見的 Transformer block 可用兩個子層來理解:attention 在位置之間交換資訊;FFN 則在每一個位置各自處理特徵。FFN 通常是兩個線性層加一個非線性函數,例如:

    FFN(x) = W₂ · ReLU(W₁x + b₁) + b₂
    

    同一層的 FFN 權重會套用到每個位置,但不同層有自己的權重。FFN 的線性變換在特徵維度上運算,不直接混合不同 token;跨位置的資訊仍由 attention 帶入。為了能和輸入相加,子層最後的特徵寬度要回到 d_model,即使 FFN 中間層較寬。

    Causal mask:限制 decoder 可以讀到哪裡

    自回歸生成不能讓預測位置讀到未來 token。以長度 4 的目標輸入來說,query 位置 i 可讀取 key 位置 j ≤ i,而 j > i 的未來位置要封鎖。若依常用分數矩陣慣例,列是 query、欄是 key,causal mask 會封鎖對角線右側:

           key 0  key 1  key 2  key 3
    query 0   可見   遮住   遮住   遮住
    query 1   可見   可見   遮住   遮住
    query 2   可見   可見   可見   遮住
    query 3   可見   可見   可見   可見
    

    對角線可見,因為它代表目前餵進 decoder 的 token。避免洩漏還需要把訓練標籤向右移一格:若要預測 [詞₁, 詞₂, EOS],decoder 輸入是 [BOS, 詞₁, 詞₂]。例如第二列以 詞₁ 為輸入,允許讀 BOS 與詞₁,但其監督目標是詞₂;它沒有讀到作為目標的詞₂本身。只加 mask、卻把未位移的正確答案直接放在同一輸入位置,仍可能洩漏答案。

    圖 4|TensorFlow 教材把 Query 放在橫軸、Key/Value 放在縱軸,藍色右上區包含當前位置的對角格,表示可連接的位置;它不是實測 attention 權重。請依圖上座標讀取三角形方向,不要把它直接描述成 query 在列時常見的左下三角。來源:TensorFlow〈Neural machine translation with a Transformer〉,TensorFlow Authors,CC BY 4.0。本文保留完整原 PNG,白底輸出,沒有更動圖元、格線、顏色或連線。改作圖依 CC BY 4.0。
    圖 5|decoder 的 Query 來自目標端表示,Key 與 Value 則來自 encoder 的來源端表示;兩端是不同輸入。圖中的五個 query 與五組 key/value 只為示意,不代表來源與目標序列必須等長,也沒有畫出「decoder 可讀未來 target」的意思。來源、作者、授權與白底完整轉檔方式同圖 4;使用 TensorFlow 原始 cross-attention PNG,圖元未改,改作圖依 CC BY 4.0。

    Causal mask 與 cross-attention 解決不同問題。前者在 decoder 的 self-attention 中禁止讀取未來目標位置;後者讓 decoder 當前表示從 encoder 的來源表示取資訊。Cross-attention 通常要遮住 source padding,但不會因此替 decoder self-attention 自動封鎖未來 target。

    Padding mask 和訓練 loss 也不同:

    機制作用範圍它沒有代替什麼

    Encoder src_key_padding_mask
    作用範圍
    讓 encoder attention 忽略 source 端 PAD keys
    它沒有代替什麼
    不會替 decoder cross-attention 自動遮 PAD;後者需 memory_key_padding_mask
    Decoder tgt_key_padding_mask
    作用範圍
    忽略 target 端作為 key/value 的 PAD 位置;可與 causal mask 一起用
    它沒有代替什麼
    不代表 PAD query 的輸出必為零
    Label loss mask/ignore_index
    作用範圍
    訓練時計算 loss 時排除不作監督的 PAD labels
    它沒有代替什麼
    不會替 attention 建立 causal 或 padding mask

    因此,「PAD token 被 attention mask 遮住」不等於「PAD label 不會計入 loss」;反過來,loss 忽略 PAD 也不會阻止模型在 attention 中讀到 PAD。這些是不同張量和不同階段的控制。本篇原型沒有實作 padding、cross-attention 或 loss。

    Transformer 訓練可以平行,生成仍按序

    訓練時,完整正確目標序列已知,先做位移,形成 decoder inputs;每個目標位置都能同時建立 query,再用 causal mask 限制各列可讀範圍。於是,同一層中的多個位置可以用矩陣運算一起算,不必像標準 RNN 那樣等待前一個 hidden state 才能算下一個位置。遮罩保留了每個位置的條件依賴,所以「平行計算」不等於「偷看未來答案」。

    但 decoder 的多層網路仍按層次傳遞:第 2 層要等第 1 層輸出。推論時,下一個 token 又依賴先前選出的 token;因此自回歸生成在 token 時間軸上仍逐步進行。實作可以用 KV cache 重用先前結果來減少重算,但這不是本文原型的內容,也不會讓下一個未知 token 在前一個 token 選定前就已知。

    圖 6|TensorFlow 教材以四層 encoder 與四層 decoder 緊湊示意 attention 堆疊;它不是原始 Transformer 論文的六層設定,也省略 FFN、residual 與位置編碼等細節。它適合看 encoder/decoder 和 attention 的層次,不用來推斷完整 block 的所有元件。來源、作者、授權與白底完整轉檔方式同圖 4;使用 TensorFlow 原始 PNG,內容未改,改作圖依 CC BY 4.0。

    以標準 dense self-attention 來說,長度 T 會形成 T × T 的 query-key 分數矩陣;平行處理這些位置不代表計算和記憶體成本固定不變。此處只談資料依賴和運算安排,沒有量測 Transformer 與 RNN 的速度、記憶體或品質,也不把理論複雜度當成所有硬體上的效能保證。

    殘差與 LayerNorm:在子層外再加一道整理

    Attention 或 FFN 子層的輸出會和其輸入做殘差相加,再做 LayerNorm。以原始論文採用的 post-norm 表示:

    post-norm: LayerNorm(x + Sublayer(x))
    

    相加前兩個張量形狀必須相同。另一種常見安排是 pre-norm:

    pre-norm: x + Sublayer(LayerNorm(x))
    

    兩者把 LayerNorm 放在不同位置,不是把公式換個寫法;每層實際輸入及梯度路徑會不同。原始論文與 D2L 圖採用 post-norm 示意,PyTorch TransformerEncoderLayer/TransformerDecoderLayer 可依 norm_first 選 post- 或 pre-norm。看到「Transformer 有 residual + LayerNorm」還不夠,若要重現特定模型,也要核對是哪種順序。

    PyTorch LayerNorm(d_model) 在每一個 token 的最後 d_model 個特徵上計算均值與變異數,不跨 batch 或序列位置;使用母體變異數(除以特徵數)並加 eps,再套上可學的 γ、β。仿射縮放與平移後,輸出均值不保證還是 0、變異數也不保證正好為 1。這點與 BatchNorm 使用 running statistics 的做法不同。

    本篇手設原型只比較一個 causal self-attention 子層的 post-norm 與 pre-norm。相同示例參數下,兩種輸出的最大絕對差為 1.6793967211269867;手算 LayerNorm 與 PyTorch 輸出的最大差為 2.220446049250313e−16。這些數字只核對此 toy 算例,不表示哪種架構普遍較好。

    從教材程式搬到自己的模型時,兩個地方要額外確認

    • eval() 不等於自動套上 causal mask。 D2L 的 PyTorch DecoderBlock 範例只在 training 分支建立 causal valid lengths;若你在 eval 模式把一整段 target prefix 一次送入,仍須確認實際呼叫提供了正確遮罩。若是一次只餵一個新 token且 state 僅含過去內容,前提不同。不要只因呼叫了 model.eval() 就假定任何輸入方式都防止未來洩漏。
    • 增量解碼要用絕對位置 offset。 D2L 教材的逐步示例在每次 forward 取 position encoding 前綴;它的簡化呼叫路徑沒有依已快取位置調整 offset。若自己把整段輸入拆成逐 token/chunk 呼叫,應檢查每塊是否加上正確絕對位置。這是對該教材程式路徑的靜態閱讀限制,不是本篇做過的完整翻譯模型實測。

    可檢查的小型機制原型

    以下程式使用四個手設 embedding、d_model=4、兩個 head、float64 與 PyTorch nn.MultiheadAttention。所有 Q/K/V 與輸出投影參數都明確覆寫;程式實際測一層 causal self-attention、位置偏移、prefix 重算與 LayerNorm/residual。已核環境為 Python 3.10.12、PyTorch 2.3.1+cu118,但只在 CPU 執行,float64、單一 thread、dropout 0、eval 並明確傳 mask。+cu118 是 PyTorch build 標記,不代表這次用了 GPU。

    可以先準備已有 PyTorch 2.3.1 的 Python 環境,把整段存成 transformer_position_mask_residual.py 後執行 python3 transformer_position_mask_residual.py。本機既有環境已跑通;新虛擬環境安裝與 pip 安裝流程沒有在本次驗證。

    四個 token 的每個 PE 向量都是四維;causal mask 是 4 × 4。對 torch.nn.MultiheadAttention 2.3 API,布林 attn_mask=True 代表封鎖該 query-key 配對,False 代表可見;這和 raw F.scaled_dot_product_attention(F 指 torch.nn.functional)的布林遮罩慣例相反,不能把一種 API 的遮罩直接搬給另一種。原型採明確 MHA mask,沒有直接呼叫或獨立測試 raw SDPA API;MHA 內部仍可能依條件選用其實作。

    以 query 位置 2 為例,它可以讀 key 0、1、2,但不能讀 key 3。兩個 head 對這列的實際權重如下:

    Headkey 0key 1key 2key 3(未來)合計

    0
    key 0
    0.385728
    key 1
    0.319920
    key 2
    0.294352
    key 3(未來)
    0
    合計
    1
    1
    key 0
    0.241557
    key 1
    0.384838
    key 2
    0.373606
    key 3(未來)
    0
    合計
    1

    將同一個完整序列一次送入 masked MHA,和逐個 prefix 從頭重算、取最後位置輸出相比,最大差是 1.1102230246251565e−16;把最後一個未來 embedding 大幅改動,前三個位置的輸出最大改變為 0。這些是有明確參數的單一小例。prefix 每一步都是從頭重算,不是 KV cache,也不是速度比較。

    張量形狀在此例為 (batch=1, sequence=4, d_model=4);切成 2 個 head 後每 head 寬度是 2。這不是完整 Transformer:沒有 FFN、cross-attention、source/target padding、tokenizer、vocabulary head、teacher-forcing loss 或訓練更新。因此程式沒有驗證翻譯品質、語料效果、GPU 效能或 KV cache 加速。完整數值輸出與驗證界線保留在本機研究紀錄;本文的程式碼區塊本身可直接另存重跑。

    完整可重跑程式

    """Small, hand-set Transformer mechanics example for DS46.
    
    CPU/float64 only. Demonstrates absolute sinusoidal positions, an explicit
    causal mask, post-norm vs pre-norm residual forms, and prefix recomputation.
    No training, corpus, cache implementation, or performance benchmark.
    """
    from __future__ import annotations
    
    import json
    import math
    import platform
    
    import torch
    from torch import nn
    
    
    def sinusoidal_position_encoding(positions: torch.Tensor, d_model: int) -> torch.Tensor:
        """Return sinusoidal encodings for explicit absolute position indices."""
        if d_model <= 0 or d_model % 2:
            raise ValueError("this example requires a positive even d_model")
        positions = torch.as_tensor(positions, dtype=torch.float64, device="cpu")
        if positions.ndim != 1:
            raise ValueError("positions must be a one-dimensional list of absolute indices")
        if torch.any(positions < 0):
            raise ValueError("positions must be nonnegative")
        feature_pairs = torch.arange(0, d_model, 2, dtype=torch.float64, device="cpu")
        denominators = torch.pow(10000.0, feature_pairs / d_model)
        angles = positions[:, None] / denominators[None, :]
        result = torch.empty((positions.numel(), d_model), dtype=torch.float64, device="cpu")
        result[:, 0::2] = torch.sin(angles)
        result[:, 1::2] = torch.cos(angles)
        return result
    
    
    def upper_triangle_causal_mask(length: int) -> torch.Tensor:
        """For nn.MultiheadAttention, True means that query-key pair is blocked."""
        if length < 1:
            raise ValueError("sequence length must be positive")
        return torch.triu(torch.ones((length, length), dtype=torch.bool), diagonal=1)
    
    
    def rounded(tensor: torch.Tensor, digits: int = 10):
        values = tensor.detach().cpu().to(torch.float64)
        if values.ndim == 0:
            return round(float(values.item()), digits)
        if values.ndim == 1:
            return [round(float(x), digits) for x in values.tolist()]
        return [rounded(row, digits) for row in values]
    
    
    def main() -> None:
        torch.set_num_threads(1)
        dtype = torch.float64
        d_model, num_heads, seq_len = 4, 2, 4
        head_dim = d_model // num_heads
        assert d_model % num_heads == 0
    
        # Four fixed token embeddings. These are illustrative inputs, not token IDs.
        embeddings = torch.tensor(
            [[[
                0.20, -0.10, 0.30, 0.40,
            ], [
                -0.30, 0.50, 0.10, -0.20,
            ], [
                0.40, 0.20, -0.50, 0.10,
            ], [
                -0.10, -0.40, 0.20, 0.30,
            ]]], dtype=dtype, device="cpu")
    
        # Explicit absolute position IDs. Match the common embedding scale + PE form.
        position_ids = torch.arange(seq_len, dtype=torch.long, device="cpu")
        pe = sinusoidal_position_encoding(position_ids, d_model)
        positioned = embeddings * math.sqrt(d_model) + pe.unsqueeze(0)
    
        # Verify a later chunk receives its absolute positions, not positions reset to 0.
        chunk_positions = torch.tensor([2, 3], dtype=torch.long, device="cpu")
        chunk_pe = sinusoidal_position_encoding(chunk_positions, d_model)
        chunk_pe_from_full = pe[2:4]
        reset_chunk_pe = sinusoidal_position_encoding(torch.tensor([0, 1]), d_model)
        offset_error = float(torch.max(torch.abs(chunk_pe - chunk_pe_from_full)).item())
        reset_error = float(torch.max(torch.abs(chunk_pe - reset_chunk_pe)).item())
        assert offset_error == 0.0
        assert reset_error > 1e-3
    
        # One explicit causal self-attention layer; all parameters are overwritten.
        mha = nn.MultiheadAttention(
            embed_dim=d_model,
            num_heads=num_heads,
            dropout=0.0,
            bias=True,
            batch_first=True,
            device="cpu",
            dtype=dtype,
        )
        w_q = torch.tensor([
            [0.40, -0.10, 0.20, 0.30],
            [0.10, 0.50, -0.20, 0.10],
            [-0.30, 0.20, 0.40, 0.10],
            [0.20, -0.40, 0.10, 0.50],
        ], dtype=dtype)
        w_k = torch.tensor([
            [0.30, 0.20, -0.10, 0.40],
            [-0.20, 0.40, 0.30, 0.10],
            [0.10, -0.30, 0.50, 0.20],
            [0.40, 0.10, 0.20, -0.20],
        ], dtype=dtype)
        w_v = torch.tensor([
            [0.50, 0.10, -0.20, 0.30],
            [0.20, -0.40, 0.30, 0.10],
            [-0.10, 0.30, 0.40, 0.20],
            [0.30, 0.20, 0.10, -0.50],
        ], dtype=dtype)
        b_q = torch.tensor([0.05, -0.02, 0.03, 0.01], dtype=dtype)
        b_k = torch.tensor([-0.03, 0.04, 0.02, -0.01], dtype=dtype)
        b_v = torch.tensor([0.01, 0.02, -0.04, 0.03], dtype=dtype)
        w_o = torch.tensor([
            [0.60, -0.20, 0.10, 0.30],
            [-0.10, 0.50, 0.20, -0.30],
            [0.20, 0.10, 0.70, -0.20],
            [0.30, -0.40, 0.10, 0.50],
        ], dtype=dtype)
        b_o = torch.tensor([0.02, -0.01, 0.03, -0.04], dtype=dtype)
        with torch.no_grad():
            mha.in_proj_weight.copy_(torch.cat([w_q, w_k, w_v], dim=0))
            mha.in_proj_bias.copy_(torch.cat([b_q, b_k, b_v], dim=0))
            mha.out_proj.weight.copy_(w_o)
            mha.out_proj.bias.copy_(b_o)
        mha.eval()
    
        causal_mask = upper_triangle_causal_mask(seq_len)
        with torch.no_grad():
            full_output, full_weights = mha(
                positioned,
                positioned,
                positioned,
                attn_mask=causal_mask,
                need_weights=True,
                average_attn_weights=False,
            )
    
            # Recompute each prefix from scratch. This is not a KV-cache implementation.
            prefix_last_outputs = []
            for end in range(1, seq_len + 1):
                prefix = positioned[:, :end, :]
                prefix_mask = upper_triangle_causal_mask(end)
                prefix_output, _ = mha(
                    prefix,
                    prefix,
                    prefix,
                    attn_mask=prefix_mask,
                    need_weights=False,
                )
                prefix_last_outputs.append(prefix_output[0, -1])
            prefix_last_outputs = torch.stack(prefix_last_outputs, dim=0)
    
            # Change only a future token; earlier query outputs must remain unchanged.
            changed_embeddings = embeddings.clone()
            changed_embeddings[0, 3] += torch.tensor([9.0, -4.0, 3.0, 1.0], dtype=dtype)
            changed_positioned = changed_embeddings * math.sqrt(d_model) + pe.unsqueeze(0)
            changed_output, _ = mha(
                changed_positioned,
                changed_positioned,
                changed_positioned,
                attn_mask=causal_mask,
                need_weights=False,
            )
    
        prefix_max_diff = float(torch.max(torch.abs(full_output[0] - prefix_last_outputs)).item())
        earlier_future_change_max_diff = float(
            torch.max(torch.abs(full_output[:, :3] - changed_output[:, :3])).item()
        )
        future_attention_max = float(
            torch.max(torch.abs(full_weights.masked_select(causal_mask[None, None, :, :]))).item()
        )
        weight_row_sums = full_weights.sum(dim=-1)
        assert prefix_max_diff < 1e-12
        assert earlier_future_change_max_diff < 1e-12
        assert future_attention_max == 0.0
        assert torch.max(torch.abs(weight_row_sums - 1.0)).item() < 1e-12
    
        # Compare the post-norm and pre-norm residual forms; these are not interchangeable.
        eps = 1e-5
        gamma = torch.tensor([1.10, 0.90, 1.20, 0.80], dtype=dtype)
        beta = torch.tensor([0.10, -0.20, 0.05, 0.30], dtype=dtype)
        layer_norm = nn.LayerNorm(d_model, eps=eps, elementwise_affine=True, device="cpu", dtype=dtype)
        with torch.no_grad():
            layer_norm.weight.copy_(gamma)
            layer_norm.bias.copy_(beta)
    
        with torch.no_grad():
            postnorm_residual_sum = positioned + full_output
            postnorm = layer_norm(postnorm_residual_sum)
            prenorm_input = layer_norm(positioned)
            prenorm_attention, _ = mha(
                prenorm_input,
                prenorm_input,
                prenorm_input,
                attn_mask=causal_mask,
                need_weights=False,
            )
            prenorm = positioned + prenorm_attention
    
            last_dim_mean = postnorm_residual_sum.mean(dim=-1, keepdim=True)
            last_dim_var = postnorm_residual_sum.var(dim=-1, unbiased=False, keepdim=True)
            manual_postnorm = (
                (postnorm_residual_sum - last_dim_mean)
                / torch.sqrt(last_dim_var + eps)
                * gamma
                + beta
            )
        manual_layernorm_diff = float(torch.max(torch.abs(postnorm - manual_postnorm)).item())
        pre_post_difference = float(torch.max(torch.abs(prenorm - postnorm)).item())
        assert manual_layernorm_diff < 1e-12
        assert pre_post_difference > 1e-6
    
        result = {
            "scope": "Four hand-set token embeddings; sinusoidal absolute positions; one hand-parameterized causal MHA layer; LayerNorm/residual arithmetic. No training, corpus, KV cache, GPU, latency, or quality benchmark.",
            "environment": {
                "python": platform.python_version(),
                "torch": torch.__version__,
                "device": "cpu",
                "dtype": "float64",
                "torch_threads": torch.get_num_threads(),
                "batch_first": True,
                "mha_dropout": 0.0,
                "mha_eval": True,
                "mha_bool_attn_mask_true_means": "blocked",
                "need_weights": True,
                "average_attn_weights": False,
            },
            "dimensions": {
                "batch": 1,
                "sequence_length": seq_len,
                "d_model": d_model,
                "num_heads": num_heads,
                "head_dim": head_dim,
                "embeddings": list(embeddings.shape),
                "position_encoding": list(pe.shape),
                "positioned_embeddings": list(positioned.shape),
                "causal_mask": list(causal_mask.shape),
                "full_output": list(full_output.shape),
                "per_head_weights": list(full_weights.shape),
                "postnorm": list(postnorm.shape),
                "prenorm": list(prenorm.shape),
            },
            "position_encoding": {
                "absolute_position_ids": position_ids.tolist(),
                "formula": "PE[pos,2i]=sin(pos/10000^(2i/d_model)); PE[pos,2i+1]=cos(pos/10000^(2i/d_model))",
                "embedding_scale": "sqrt(d_model)",
                "pe_rows": rounded(pe),
                "positioned_input_first_token": rounded(positioned[0, 0]),
                "offset_check_positions": chunk_positions.tolist(),
                "offset_pe_max_diff_vs_full_sequence_rows_2_3": offset_error,
                "incorrect_reset_to_positions_0_1_max_diff": reset_error,
            },
            "causal_mask": {
                "true_means": "block this query-key pair for torch.nn.MultiheadAttention 2.3",
                "matrix": causal_mask.tolist(),
                "diagonal_is_visible": True,
                "full_sequence_output": rounded(full_output[0]),
                "per_head_attention_weights": rounded(full_weights[0]),
                "per_query_key_weight_sums": rounded(weight_row_sums[0]),
                "max_weight_on_blocked_future_positions": future_attention_max,
                "full_masked_pass_vs_independent_prefix_recomputation_max_diff": prefix_max_diff,
                "earlier_output_change_after_perturbing_future_token_max_diff": earlier_future_change_max_diff,
                "prefix_recomputation_note": "Each prefix is rerun from scratch at its original position IDs; this is not cached decoding or a speed comparison.",
            },
            "residual_and_layernorm": {
                "post_norm_formula": "LayerNorm(x + Sublayer(x))",
                "pre_norm_formula": "x + Sublayer(LayerNorm(x))",
                "chosen_demo_path": "post_norm",
                "residual_sum": rounded(postnorm_residual_sum[0]),
                "epsilon": eps,
                "normalized_axis": -1,
                "variance_convention": "biased variance (unbiased=False) across the final d_model feature axis for each token",
                "gamma": rounded(gamma),
                "beta": rounded(beta),
                "postnorm_output": rounded(postnorm[0]),
                "postnorm_feature_means_after_affine": rounded(postnorm.mean(dim=-1)[0]),
                "postnorm_feature_variances_after_affine_biased": rounded(postnorm.var(dim=-1, unbiased=False)[0]),
                "manual_layernorm_max_diff": manual_layernorm_diff,
                "pre_vs_post_norm_max_diff_same_demo_parameters": pre_post_difference,
            },
            "checks": "PASS: explicit positions retain absolute offsets; MHA future-mask weights are zero; full causal sequence rows match same-token prefix recomputation; changing a future token leaves earlier outputs unchanged; post-norm LayerNorm matches manual final-feature-axis biased-variance computation; pre-norm and post-norm outputs differ.",
        }
        print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
    
    
    if __name__ == "__main__":
        main()
    

    來源與延伸閱讀

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →