t27.aiEnglish

Внимание

Вы узнаете

Как внимание сравнивает каждый запрос с каждым ключом, что даёт softmax по каждой строке и что убирает причинная маска.

Внимание позволяет каждой позиции решить, какие другие позиции читать. Каждый запрос сравнивается с каждым ключом, q . k / sqrt(d), и softmax превращает каждую строку оценок в веса с суммой 1; выход это значения, усреднённые этими весами. Две головы делают это на двух половинах вектора. С причинной маской позиция не может читать будущее: в голове 0 позиция 0 читает только себя, позиция 1 делит 0.452 и 0.548, а позиция 2 распределяет 0.230, 0.321 и 0.449. Без маски каждая строка распределяется по всем трём ключам, от 0.21 до 0.47. Спека attention_mechanism.t27 вычисляет каждый вес на своей фикстуре.

Попробовать

Выключите и включите маску и прочитайте строку 0; затем сравните две головы в строке 2.

Открыть интерактивный урок →

Attention: how a word decides which other words to read
Attention: how a word decides which other words to read ↗

Two heads of attention over three positions: each row is how much one query reads each key. Switch on the causal mask and a position can no longer read the future.

specs/ml/recurrent/attention_mechanism.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Memory-efficient attention for long sequences | φ² + 1/φ² = 3 | TRINITY

// #8980: redeclared `var i` in one scope four times, called `@infinity()` and
// `@random()` (not Zig), and indexed every row as head_dim wide inside d_model-wide
// rows. backward also skipped the softmax Jacobian: it pushed dL/dweights straight
// into the scores. It is now multi-head scaled dot-product attention over seq x
// d_model rows. Head h reads columns h*hd .. (h+1)*hd, the weights are
// heads x seq x seq, and causal masking is optional. backward is the exact
// gradient, checked against finite differences. Dropout is carried in the config
// and not applied: this is the inference computation.

module Attention;
    use base::types;
    use math::constants;

    // ═══════════════════════════════════════════════════════════
    // 1. Constants
    // ═══════════════════════════════════════════════════════════

    const NEG_INF : f32 = -1.0e30;

    // ═══════════════════════════════════════════════════════════
    // 2. Types
    // ═══════════════════════════════════════════════════════════

    pub const AttentionConfig = struct {
        d_model : u32,
        num_heads : u32,
        dropout : f32,
        causal : bool,
    };

    pub const AttentionOutput = struct {
        output : []f32,
        weights : []f32,
    };

    // ═══════════════════════════════════════════════════════════
    // 3. Core Functions
    // ═══════════════════════════════════════════════════════════

    fn seq_of(query: []const f32, config: AttentionConfig) -> usize {
        return query.len / config.d_model;
    }

    fn scale_of(config: AttentionConfig) -> f32 {
        return 1.0 / sqrt(@as(f32, @floatFromInt(config.d_model / config.num_heads)));
    }

    fn forward(query: []const f32, key: []const f32, value: []const f32, output: []f32, weights: []f32, config: AttentionConfig) -> void {
        const n = seq_of(query, config);
        const d : usize = config.d_model;
        const hd : usize = config.d_model / config.num_heads;
        const scale = scale_of(config);
        var h : usize = 0;
        while (h < config.num_heads) {
            var i : usize = 0;
            while (i < n) {
                const row = weights[(h * n + i) * n .. (h * n + i + 1) * n];
                var max_s : f32 = NEG_INF;
                var j : usize = 0;
                while (j < n) {
                    var score : f32 = NEG_INF;
                    if (!(config.causal and j > i)) {
                        score = 0.0;
                        var k : usize = 0;
                        while (k < hd) {
                            score = score + query[i * d + h * hd + k] * key[j * d + h * hd + k];
                            k = k + 1;
                        }
                        score = score * scale;
                    }
                    row[j] = score;
                    max_s = @max(max_s, score);
                    j = j + 1;
                }
                var sum : f32 = 0.0;
                j = 0;
                while (j < n) {
                    row[j] = exp(row[j] - max_s);
                    sum = sum + row[j];
                    j = j + 1;
                }
                j = 0;
                while (j < n) {
                    row[j] = row[j] / sum;
                    j = j + 1;
                }
                var k : usize = 0;
                while (k < hd) {
                    var acc : f32 = 0.0;
                    j = 0;
                    while (j < n) {
                        acc = acc + row[j] * value[j * d + h * hd + k];
                        j = j + 1;
                    }
                    output[i * d + h * hd + k] = acc;
                    k = k + 1;
                }
                i = i + 1;
            }
            h = h + 1;
        }
    }

    // dV_j += P_ij dO_i;  dP_ij = dO_i . V_j;  dS_ij = P_ij (dP_ij - sum_k P_ik dP_ik);
    // dQ_i += scale dS_ij K_j;  dK_j += scale dS_ij Q_i  (per head; masked P_ij are 0)
    fn backward(grad_output: []const f32, query: []const f32, key: []const f32, value: []const f32, attn_weights: []f32, grad_query: []f32, grad_key: []f32, grad_value: []f32, config: AttentionConfig) -> void {
        const n = seq_of(query, config);
        const d : usize = config.d_model;
        const hd : usize = config.d_model / config.num_heads;
        const scale = scale_of(config);
        var x : usize = 0;
        while (x < n * d) {
            grad_query[x] = 0.0;
            grad_key[x] = 0.0;
            grad_value[x] = 0.0;
            x = x + 1;
        }
        var h : usize = 0;
        while (h < config.num_heads) {
            var i : usize = 0;
            while (i < n) {
                const p = attn_weights[(h * n + i) * n .. (h * n + i + 1) * n];
                var row_dot : f32 = 0.0;
                var j : usize = 0;
                while (j < n) {
                    var dp : f32 = 0.0;
                    var k : usize = 0;
                    while (k < hd) {
                        dp = dp + grad_output[i * d + h * hd + k] * value[j * d + h * hd + k];
                        grad_value[j * d + h * hd + k] = grad_value[j * d + h * hd + k] + p[j] * grad_output[i * d + h * hd + k];
                        k = k + 1;
                    }
                    row_dot = row_dot + p[j] * dp;
                    j = j + 1;
                }
                j = 0;
                while (j < n) {
                    var dp : f32 = 0.0;
                    var k : usize = 0;
                    while (k < hd) {
                        dp = dp + grad_output[i * d + h * hd + k] * value[j * d + h * hd + k];
                        k = k + 1;
                    }
                    const ds = p[j] * (dp - row_dot) * scale;
                    k = 0;
                    while (k < hd) {
                        grad_query[i * d + h * hd + k] = grad_query[i * d + h * hd + k] + ds * key[j * d + h * hd + k];
                        grad_key[j * d + h * hd + k] = grad_key[j * d + h * hd + k] + ds * query[i * d + h * hd + k];
                        k = k + 1;
                    }
                    j = j + 1;
                }
                i = i + 1;
            }
            h = h + 1;
        }
    }

    fn cfg(causal: bool, dropout: f32) -> AttentionConfig {
        return AttentionConfig{ .d_model = 4, .num_heads = 2, .dropout = dropout, .causal = causal };
    }

    // The fixture: seq 3, d_model 4, 2 heads (columns 0-1 and 2-3); q / k / v scaled down so the
    // softmax is not saturated. which: 0 = output, 1 = weights.
    fn fixture(q: []f32, k: []f32, v: []f32) -> void {
        var x : usize = 0;
        while (x < 12) {
            q[x] = @as(f32, @floatFromInt(x + 1)) / 10.0;
            k[x] = @as(f32, @floatFromInt(x / 4 + 1)) / 4.0 - @as(f32, @floatFromInt(x % 2)) / 3.0;
            v[x] = @as(f32, @floatFromInt(x + 1)) / 10.0;
            x = x + 1;
        }
    }

    fn forward_at(causal: bool, which: u8, index: usize) -> f32 {
        var q : [12]f32 = undefined;
        var k : [12]f32 = undefined;
        var v : [12]f32 = undefined;
        fixture(&q, &k, &v);
        var out : [12]f32 = [9.0] ** 12;
        var w : [18]f32 = [9.0] ** 18;
        forward(&q, &k, &v, &out, &w, cfg(causal, 0.0));
        if (which == 0) {
            return out[index];
        }
        return w[index];
    }

    // L = sum_x g_x O_x with g_x = (x + 1) / 10, one entry of q, k or v moved by delta
    // (which: 0 = q, 1 = k, 2 = v)
    fn loss(which: u8, index: usize, delta: f32) -> f32 {
        var q : [12]f32 = undefined;
        var k : [12]f32 = undefined;
        var v : [12]f32 = undefined;
        fixture(&q, &k, &v);
        if (which == 0) {
            q[index] = q[index] + delta;
        }
        if (which == 1) {
            k[index] = k[index] + delta;
        }
        if (which == 2) {
            v[index] = v[index] + delta;
        }
        var out : [12]f32 = undefined;
        var w : [18]f32 = undefined;
        forward(&q, &k, &v, &out, &w, cfg(false, 0.0));
        var total : f32 = 0.0;
        var x : usize = 0;
        while (x < 12) {
            total = total + @as(f32, @floatFromInt(x + 1)) / 10.0 * out[x];
            x = x + 1;
        }
        return total;
    }

    fn grad(which: u8, index: usize, dropout: f32) -> f32 {
        var q : [12]f32 = undefined;
        var k : [12]f32 = undefined;
        var v : [12]f32 = undefined;
        fixture(&q, &k, &v);
        var out : [12]f32 = undefined;
        var w : [18]f32 = undefined;
        forward(&q, &k, &v, &out, &w, cfg(false, dropout));
        var g : [12]f32 = undefined;
        var x : usize = 0;
        while (x < 12) {
            g[x] = @as(f32, @floatFromInt(x + 1)) / 10.0;
            x = x + 1;
        }
        var gq : [12]f32 = undefined;
        var gk : [12]f32 = undefined;
        var gv : [12]f32 = undefined;
        backward(&g, &q, &k, &v, &w, &gq, &gk, &gv, cfg(false, dropout));
        if (which == 0) {
            return gq[index];
        }
        if (which == 1) {
            return gk[index];
        }
        return gv[index];
    }

    fn slope(which: u8, index: usize) -> f32 {
        return (loss(which, index, 0.001) - loss(which, index, -0.001)) / 0.002;
    }

    fn valid_input() -> u32 {
        return cfg(false, 0.0).d_model % cfg(false, 0.0).num_heads;
    }

    // ═══════════════════════════════════════════════════════════
    // TDD: Tests (from .tri behaviors)
    // ═══════════════════════════════════════════════════════════

    // Each weight row is a distribution, and each output is a weighted mean of the values
    // (so it lies between the smallest and largest value in its column)
    test forward_basic_case {
        const row_sum = forward_at(false, 1, 0) + forward_at(false, 1, 1) + forward_at(false, 1, 2);
        assert(@abs(row_sum - 1.0) < 0.000001);
        assert(forward_at(false, 0, 0) + forward_at(false, 0, 1) + forward_at(false, 0, 2) + forward_at(false, 0, 3) > 0.0);  // computed a meaningful output
        assert(forward_at(false, 0, 0) > 0.1 and forward_at(false, 0, 0) < 0.9);
    }

    // dQ, dK and dV against central differences of L = g . O
    test backward_basic_case {
        assert(@abs(grad(0, 0, 0.0) - slope(0, 0)) < 0.001);
        assert(@abs(grad(0, 7, 0.0) - slope(0, 7)) < 0.001);
        assert(@abs(grad(1, 5, 0.0) - slope(1, 5)) < 0.001);
        assert(@abs(grad(2, 10, 0.0) - slope(2, 10)) < 0.001);
        assert(grad(0, 0, 0.0) + grad(0, 1, 0.0) + grad(0, 2, 0.0) + grad(0, 3, 0.0) != 0.0);  // computed gradients
    }

    // Position 0 attends only to itself, position 1 to positions 0 and 1
    test forward_causal_masking {
        assert(forward_at(true, 1, 0) == 1.0);
        assert(forward_at(true, 1, 1) == 0.0 and forward_at(true, 1, 2) == 0.0);
        assert(forward_at(true, 1, 1 * 3 + 2) < 0.999);
        assert(forward_at(true, 1, 1 * 3 + 2) == 0.0);
        assert(forward_at(true, 0, 0) == 0.1);
    }

    // Dropout is not applied here (inference), so it leaves the gradients unchanged
    test backward_dropout {
        assert(grad(2, 3, 0.5) == grad(2, 3, 0.0));
        assert(grad(0, 11, 0.5) == grad(0, 11, 0.0));
    }

    // ═══════════════════════════════════════════════════════════
    // TDD: Invariants (from .tri constraints)
    // ═══════════════════════════════════════════════════════════

    // d_model % num_heads == 0
    invariant attention_constraint_0
        assert valid_input() == 0

    // 0 <= dropout < 1
    invariant attention_constraint_1
        assert cfg(false, 0.5).dropout >= 0.0 and cfg(false, 0.5).dropout < 1.0

    // query.shape == key.shape == value.shape: seq is read from the query
    invariant attention_constraint_2
        assert cfg(false, 0.0).d_model == 4

Открыть спеку урока в плеере ↗

Все уроки