From 519c007e86cac839cc1cd0adeaf105efe0c113fd Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 13:00:28 +0700 Subject: [PATCH 1/3] spec(ml): train_hslm speed=4 -- int8 trit forward, one scale per row, error bounded against speed=2 (Closes #10875) speed=4 is speed 3's window with every forward ternary matmul in integers: activations rounded to int8 with one scale per row, trits transposed once per call, sixteen independent i32 accumulators, output = acc * scale. The backward stays speed 3's f64 code. Tests bound the kernel error (proved per-output bound), logits, k-position loss and gradients against speed 2, and re-derive the lab loss-curve gap and tok/s from recorded constants. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/ml_train_hslm.json | 16 +- specs/ml/train_hslm.t27 | 439 +++++++++++++++++++++++++++++- 2 files changed, 442 insertions(+), 13 deletions(-) diff --git a/.trinity/seals/ml_train_hslm.json b/.trinity/seals/ml_train_hslm.json index ae1d379241..53453d7aae 100644 --- a/.trinity/seals/ml_train_hslm.json +++ b/.trinity/seals/ml_train_hslm.json @@ -19,23 +19,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:9d92144a94228e46d3131ea7f5fe1bc86ead3a721add372e0051fee58317bc46", - "gen_hash_rust": "sha256:fa4751c35bca1feefaad3cd55dc1112fd7df2db4e6c5d7e21c34a024275d4285", - "gen_hash_verilog": "sha256:cf563313b4759cb371ab1a6f4aa5ad22e9770984d31d27bb2b7b448c48a54817", - "gen_hash_zig": "sha256:bbc14e0e851557974231ac8219526c2b75127e280d325f8c725f2131661c5874", + "gen_hash_c": "sha256:ceedef84e0e719b3e6c65b490748115e8d9f4202b9bb49dd991389839c3b312c", + "gen_hash_rust": "sha256:c31680740ca123012b226b6df09915b79b59c4117ba598bacaf773d6f400bdd6", + "gen_hash_verilog": "sha256:782d456603a228e656ebb47361f67f29a5ed4696aa613c539ac2fdde2eafcd30", + "gen_hash_zig": "sha256:1c3cf9b8516a7ad1aaaea70758260092783f0d69a5ed61dce754bf8defd5db9d", "module": "train_hslm", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T05:13:11Z", + "sealed_at": "2026-10-19T06:08:20Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:8218a7d2003b4b78582badf5e5b3bb42117455e722a4f4805b18963c2c092859", + "spec_hash": "sha256:68983f776ddf327d028cc9957e8ccde6d9200c9a681f3ee9be7b62deca00316d", "spec_path": "specs/ml/train_hslm.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 27, - "total": 27, + "passed": 33, + "total": 33, "vacuous": 0 }, "toolchain": { diff --git a/specs/ml/train_hslm.t27 b/specs/ml/train_hslm.t27 index 30ce0fb131..3a4330fe52 100644 --- a/specs/ml/train_hslm.t27 +++ b/specs/ml/train_hslm.t27 @@ -61,7 +61,10 @@ ; computes the speed 2 window layer by layer over all 27 positions, so each ternary matrix and each ; gradient row is read once per window instead of once per position, with every sum in speed 2's ; order. The tests check that speed 1, 2 and 3 produce the same logits as speed 0 to the bit, and that -; speed 3 produces the same logits, loss and gradients as speed 2 to the bit. The model is not reduced. +; speed 3 produces the same logits, loss and gradients as speed 2 to the bit. speed=4 (#10875) is +; speed 3 with every forward ternary matmul in integers (int8 activations, one scale per row, an i32 +; accumulator); it changes the bits, so its tests bound the error against speed 2 instead, and the +; lab loss curves of speed 2 and speed 4 sit next to each other in section 1. The model is not reduced. ; WHAT DIFFERS FROM THE ZIG TRAINER (trinity-training @ 3e9648c, run R5): f64, not f32; LAMB, not ; AdamW; one global clip, not per tensor; linear warmup + cosine to 1e-6, not the phi schedule; the ; embedding table and RMSNorm gains are trained (the trainer froze the table); the gradient is that of @@ -289,6 +292,10 @@ fn ternary_matmul(input: []const f64, weights: []const Trit, output: []f64, in_d tr_matmul_ref(input, weights, output, in_dim, out_dim); return; } + if (speed_level == 4) { + tr4_matmul(input, weights, output, in_dim, out_dim, 0, 1); + return; + } var i : usize = 0; while (i < out_dim) { output[i] = 0.0; @@ -450,7 +457,7 @@ fn tr_prefix_forward(token: usize, position: usize) -> void { /// One 27-token window: every position in order (each attends to the cache of the earlier ones); /// the logits of the last position land in `logits`, its activations stay for hslm_backward fn tr_window_forward(tokens: []const u16, start: usize, logits: []f64) -> void { - if (speed_level == 3) { + if (speed_level >= 3) { tr3_forward(tokens, start, CONTEXT_LEN - 1, CONTEXT_LEN - 1); hslm_forward(@as(usize, @intCast(tokens[start + CONTEXT_LEN - 1])), CONTEXT_LEN - 1, logits); return; @@ -495,7 +502,7 @@ fn tr_window_loss(tokens: []const u16, start: usize) -> f64 { /// Forward, loss and backward of one window under the k-position objective (1 <= k <= 27); /// gradients accumulate in nn/hslm's gw_* and g_*; returns the mean of the k losses fn tr_window_grad_k(tokens: []const u16, start: usize, k: usize) -> f64 { - if (speed_level == 3) { + if (speed_level >= 3) { return tr3_window_grad(tokens, start, k); } const inv_k : f64 = 1.0 / @as(f64, @floatFromInt(k)); @@ -528,7 +535,7 @@ fn tr_window_grad_k(tokens: []const u16, start: usize, k: usize) -> f64 { /// cross-entropy of each of the last k against its next token, their mean fn tr_window_loss_k(tokens: []const u16, start: usize, k: usize) -> f64 { var sum : f64 = 0.0; - if (speed_level == 3) { + if (speed_level >= 3) { tr3_forward(tokens, start, CONTEXT_LEN, 0); var q : usize = CONTEXT_LEN - k; while (q < CONTEXT_LEN) { @@ -582,6 +589,29 @@ const TR3_LAB_S3_TOK_S : f64 = 478.1; const TR3_LAB_S2_TRAIN_S_AGAIN : f64 = 55.62; const TR3_LAB_S3_TRAIN_S_AGAIN : f64 = 37.27; const TR3_LAB_SPEEDUP : f64 = 1.478; +; SPEED 4 MEASURED (Railway lab, 2026-10-19, 48 cores under a load average of 47-66 from other +; sessions). Throughput: train.cfg loss_positions=27 steps=10 total=32000 seed=1729 eval_every=10 +; eval_windows=1, the ReleaseFast binary of this spec, speed=2, 3 and 4 started at the same moment, +; twice; all three printed the same first and last loss (6.6079, 6.6012) at 10 steps. tok/s is +; 17820 tokens over train_s. Curves: loss_positions=27 steps=300 total=300 warmup=50 seed=1729 +; eval_every=25 eval_windows=64, speed=2 and speed=4 side by side, the speed=3 run printing the +; speed=2 digits to the last one; each array holds the 13 evals at steps 0, 25, ..., 300 (the train +; loss is the mean of the last 27 positions, and has no value at step 0). +const TR4_LAB_S2_TRAIN_S : f64 = 65.63; +const TR4_LAB_S3_TRAIN_S : f64 = 42.20; +const TR4_LAB_S4_TRAIN_S : f64 = 32.48; +const TR4_LAB_S2_TOK_S : f64 = 271.5; +const TR4_LAB_S3_TOK_S : f64 = 422.3; +const TR4_LAB_S4_TOK_S : f64 = 548.6; +const TR4_LAB_S2_TRAIN_S_AGAIN : f64 = 66.46; +const TR4_LAB_S3_TRAIN_S_AGAIN : f64 = 44.94; +const TR4_LAB_S4_TRAIN_S_AGAIN : f64 = 34.15; +const TR4_LAB_SPEEDUP_OVER_S2 : f64 = 2.021; +const TR4_LAB_SPEEDUP_OVER_S3 : f64 = 1.299; +const TR4_CURVE_S2_VALID : [13]f64 = [6.5962, 6.5874, 6.5627, 6.5040, 6.4470, 6.4119, 6.3826, 6.3630, 6.3486, 6.3407, 6.3363, 6.3342, 6.3340]; +const TR4_CURVE_S4_VALID : [13]f64 = [6.5962, 6.5874, 6.5629, 6.5039, 6.4469, 6.4120, 6.3822, 6.3630, 6.3484, 6.3410, 6.3362, 6.3342, 6.3336]; +const TR4_CURVE_S2_TRAIN : [13]f64 = [0.0, 6.5840, 6.5341, 6.4625, 6.3993, 6.3641, 6.3443, 6.3215, 6.2985, 6.2884, 6.2839, 6.2732, 6.2822]; +const TR4_CURVE_S4_TRAIN : [13]f64 = [0.0, 6.5840, 6.5341, 6.4625, 6.3994, 6.3641, 6.3444, 6.3215, 6.2987, 6.2883, 6.2839, 6.2732, 6.2822]; const TR3_TILE : usize = 81; var b3_tok : [CONTEXT_LEN]usize = undefined; var b3_x : [NUM_BLOCKS * CONTEXT_LEN * EMBED_DIM]f64 = undefined; @@ -619,6 +649,10 @@ var b3_da : [CONTEXT_LEN]f64 = undefined; /// read once for all rows; a column tile of TR3_TILE keeps that tile of every row in L1. Per output /// the terms are added in the order j = 0, 1, ... from +0.0, as ternary_matmul adds them. fn tr3_matmul(x: []const f64, weights: []const Trit, y: []f64, in_dim: usize, out_dim: usize, p_lo: usize, p_hi: usize) -> void { + if (speed_level == 4) { + tr4_matmul(x, weights, y, in_dim, out_dim, p_lo, p_hi); + return; + } if (p_lo >= p_hi) { return; } @@ -666,6 +700,145 @@ fn tr3_matmul(x: []const f64, weights: []const Trit, y: []f64, in_dim: usize, ou } } +; SPEED 4: THE INT8 TRIT FORWARD (#10875). Speed 3's window with every forward ternary matmul +; (Q, K, V, O, up, down, the head, and the last position's hslm_forward) done in integers: each row +; of activations x[p] gets one scale s = max_j |x[p][j]| / 127 and is rounded to int8, q = round(x / s) +; (half away from zero) and held as i16; each call copies the trits once into a transposed i16 matrix +; (column c contiguous), so every output is one i16 dot product; the products add in i32 +; accumulators, exact in any order (|sum| <= 127 * 729 < 2^17), so the lanes may be split and joined +; however the vectorizer likes without changing a bit; the output is s * sum. The backward stays speed 3's f64 code, on the activations this forward made. This changes +; the bits, so the tests bound the error instead of comparing bits. THE BOUND, per output: +; |s * sum_j q_j w_j - sum_j x_j w_j| <= sum_j |w_j| |s q_j - x_j| <= (s / 2) nnz_c +; = (max_j |x[p][j]| / 254) nnz_c, nnz_c the nonzero trits in column c; a test checks every output of +; three matmuls of a real window against it. +const TR4_QMAX : f64 = 127.0; +var b4_q : [CONTEXT_LEN * FF_DIM]i16 = undefined; +var b4_s : [CONTEXT_LEN]f64 = undefined; +var b4_wt : [FF_DIM * EMBED_DIM]i16 = undefined; + +/// y[p] = s_p * (q[p] W) for rows p_lo..p_hi, q[p] the int8 image of x[p] with the one scale s_p; +/// W transposed into b4_wt once per call, then one i16 dot product per output in sixteen i32 lane +/// accumulators (a0..a15, one per j mod 16), which the vectorizer packs into one register and adds +/// across only once per output; with one accumulator it reduces the lanes every eight products and +/// the kernel ran no faster than speed 3's f64 (measured on the lab, 2026-10-19) +fn tr4_matmul(x: []const f64, weights: []const Trit, y: []f64, in_dim: usize, out_dim: usize, p_lo: usize, p_hi: usize) -> void { + if (p_lo >= p_hi) { + return; + } + var p : usize = p_lo; + while (p < p_hi) { + const xo : usize = p * in_dim; + var m : f64 = 0.0; + var j : usize = 0; + while (j < in_dim) { + const a : f64 = abs(x[xo + j]); + if (a > m) { + m = a; + } + j = j + 1; + } + var inv : f64 = 0.0; + b4_s[p] = 0.0; + if (m > 0.0 and m - m == 0.0) { + inv = TR4_QMAX / m; + b4_s[p] = m / TR4_QMAX; + } + j = 0; + while (j < in_dim) { + const v : f64 = x[xo + j] * inv; + var qi : i32 = 0; + if (v >= 0.5) { + qi = 127; + if (v < 126.5) { + qi = @intFromFloat(v + 0.5); + } + } else if (v <= 0.0 - 0.5) { + qi = 0 - 127; + if (v > 0.0 - 126.5) { + qi = 0 - @as(i32, @intFromFloat(0.5 - v)); + } + } + b4_q[xo + j] = @intCast(qi); + j = j + 1; + } + p = p + 1; + } + var c0 : usize = 0; + while (c0 < out_dim) { + var c1 : usize = c0 + TR3_TILE; + if (c1 > out_dim) { + c1 = out_dim; + } + var j : usize = 0; + while (j < in_dim) { + const row : usize = j * out_dim; + var c : usize = c0; + while (c < c1) { + b4_wt[c * in_dim + j] = @as(i16, @intFromEnum(weights[row + c])); + c = c + 1; + } + j = j + 1; + } + c0 = c1; + } + p = p_lo; + while (p < p_hi) { + const qo : usize = p * in_dim; + const qr : []const i16 = b4_q[qo..qo + in_dim]; + const yo : usize = p * out_dim; + const sp : f64 = b4_s[p]; + var c : usize = 0; + while (c < out_dim) { + const wo : usize = c * in_dim; + const wr : []const i16 = b4_wt[wo..wo + in_dim]; + var a0 : i32 = 0; + var a1 : i32 = 0; + var a2 : i32 = 0; + var a3 : i32 = 0; + var a4 : i32 = 0; + var a5 : i32 = 0; + var a6 : i32 = 0; + var a7 : i32 = 0; + var a8 : i32 = 0; + var a9 : i32 = 0; + var a10 : i32 = 0; + var a11 : i32 = 0; + var a12 : i32 = 0; + var a13 : i32 = 0; + var a14 : i32 = 0; + var a15 : i32 = 0; + var j : usize = 0; + while (j + 16 <= in_dim) { + a0 = a0 + @as(i32, qr[j + 0]) * @as(i32, wr[j + 0]); + a1 = a1 + @as(i32, qr[j + 1]) * @as(i32, wr[j + 1]); + a2 = a2 + @as(i32, qr[j + 2]) * @as(i32, wr[j + 2]); + a3 = a3 + @as(i32, qr[j + 3]) * @as(i32, wr[j + 3]); + a4 = a4 + @as(i32, qr[j + 4]) * @as(i32, wr[j + 4]); + a5 = a5 + @as(i32, qr[j + 5]) * @as(i32, wr[j + 5]); + a6 = a6 + @as(i32, qr[j + 6]) * @as(i32, wr[j + 6]); + a7 = a7 + @as(i32, qr[j + 7]) * @as(i32, wr[j + 7]); + a8 = a8 + @as(i32, qr[j + 8]) * @as(i32, wr[j + 8]); + a9 = a9 + @as(i32, qr[j + 9]) * @as(i32, wr[j + 9]); + a10 = a10 + @as(i32, qr[j + 10]) * @as(i32, wr[j + 10]); + a11 = a11 + @as(i32, qr[j + 11]) * @as(i32, wr[j + 11]); + a12 = a12 + @as(i32, qr[j + 12]) * @as(i32, wr[j + 12]); + a13 = a13 + @as(i32, qr[j + 13]) * @as(i32, wr[j + 13]); + a14 = a14 + @as(i32, qr[j + 14]) * @as(i32, wr[j + 14]); + a15 = a15 + @as(i32, qr[j + 15]) * @as(i32, wr[j + 15]); + j = j + 16; + } + var acc : i32 = a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15; + while (j < in_dim) { + acc = acc + @as(i32, qr[j]) * @as(i32, wr[j]); + j = j + 1; + } + y[yo + c] = @as(f64, @floatFromInt(acc)) * sp; + c = c + 1; + } + p = p + 1; + } +} + /// dx[p] += W dy[p] for rows p_lo..p_hi: the weight row j is turned into floats once, then each row /// takes the four partial sums of ternary_matmul_t_add fn tr3_t_add(dy: []const f64, weights: []const Trit, dx: []f64, in_dim: usize, out_dim: usize, p_lo: usize, p_hi: usize) -> void { @@ -2403,7 +2576,7 @@ fn tr_eval_line(step: u64, have_train: bool, avg_loss: f64, valid: f64, valid_fu fn tr_main() -> i32 { const t_begin : f64 = tr_now(); tr_read_cfg(); - speed_level = @intCast(cfg_speed % 4); + speed_level = @intCast(cfg_speed % 5); if (cfg_gradcheck > 0) { return tr_gradcheck(); } @@ -2828,6 +3001,222 @@ fn tr_speed3_speedup_again() -> f64 { return TR3_LAB_S2_TRAIN_S_AGAIN / TR3_LAB_S3_TRAIN_S_AGAIN; } +/// The largest |speed 4 - speed 2| over the 13 recorded held-out losses of the 300-step curves +fn tr4_curve_valid_gap() -> f64 { + var g : f64 = 0.0; + var i : usize = 0; + while (i < 13) { + const d : f64 = abs(TR4_CURVE_S2_VALID[i] - TR4_CURVE_S4_VALID[i]); + if (d > g) { + g = d; + } + i = i + 1; + } + return g; +} + +/// The same over the 12 recorded train losses (step 0 has none) +fn tr4_curve_train_gap() -> f64 { + var g : f64 = 0.0; + var i : usize = 1; + while (i < 13) { + const d : f64 = abs(TR4_CURVE_S2_TRAIN[i] - TR4_CURVE_S4_TRAIN[i]); + if (d > g) { + g = d; + } + i = i + 1; + } + return g; +} + +/// How far the held-out loss fell over the 300 steps at speed 4 (so the curves are not flat) +fn tr4_curve_fall() -> f64 { + return TR4_CURVE_S4_VALID[0] - TR4_CURVE_S4_VALID[12]; +} + +/// The largest gap between a recorded speed 2 / 3 / 4 tok/s and the one its train_s gives +fn tr4_toks_gap() -> f64 { + const a : f64 = abs(TR3_LAB_TOKENS / TR4_LAB_S2_TRAIN_S - TR4_LAB_S2_TOK_S); + const b : f64 = abs(TR3_LAB_TOKENS / TR4_LAB_S3_TRAIN_S - TR4_LAB_S3_TOK_S); + const c : f64 = abs(TR3_LAB_TOKENS / TR4_LAB_S4_TRAIN_S - TR4_LAB_S4_TOK_S); + var g : f64 = a; + if (b > g) { + g = b; + } + if (c > g) { + g = c; + } + return g; +} + +/// |recorded speed-up - the ratio of the recorded tok/s|, over speed 2 and over speed 3 +fn tr4_ratio_gap() -> f64 { + const a : f64 = abs(TR4_LAB_S4_TOK_S / TR4_LAB_S2_TOK_S - TR4_LAB_SPEEDUP_OVER_S2); + const b : f64 = abs(TR4_LAB_S4_TOK_S / TR4_LAB_S3_TOK_S - TR4_LAB_SPEEDUP_OVER_S3); + if (a > b) { + return a; + } + return b; +} + +/// The speed 4 / speed 3 speed-up of the second pair of runs, from its train_s +fn tr4_speedup_again() -> f64 { + return TR4_LAB_S3_TRAIN_S_AGAIN / TR4_LAB_S4_TRAIN_S_AGAIN; +} + +var s4_y3 : [CONTEXT_LEN * VOCAB_SIZE]f64 = undefined; +var s4_y4 : [CONTEXT_LEN * VOCAB_SIZE]f64 = undefined; +var s4_m : [CONTEXT_LEN]f64 = undefined; +var s4_nnz : [VOCAB_SIZE]f64 = undefined; +var s4_ratio : f64 = 0.0; +var s4_err : f64 = 0.0; +var s4_scale : f64 = 0.0; + +/// One matmul over the 27 rows of x at speed 3 (f64) and at speed 4 (int8): s4_ratio becomes the +/// larger of itself and the largest |speed 4 - speed 3| / ((max_j |x[p][j]| / 254) nnz_c + 1e-9 +/// max_j |x[p][j]|) over every output; an output whose bound is 0 and differs counts as ratio 2 +fn tr4_bound_check(x: []const f64, weights: []const Trit, in_dim: usize, out_dim: usize) -> void { + speed_level = 3; + tr3_matmul(x, weights, &s4_y3, in_dim, out_dim, 0, CONTEXT_LEN); + speed_level = 4; + tr3_matmul(x, weights, &s4_y4, in_dim, out_dim, 0, CONTEXT_LEN); + speed_level = 2; + var c : usize = 0; + while (c < out_dim) { + var n : f64 = 0.0; + var j : usize = 0; + while (j < in_dim) { + if (@as(i64, @intFromEnum(weights[j * out_dim + c])) != 0) { + n = n + 1.0; + } + j = j + 1; + } + s4_nnz[c] = n; + c = c + 1; + } + var p : usize = 0; + while (p < CONTEXT_LEN) { + var m : f64 = 0.0; + var j : usize = 0; + while (j < in_dim) { + if (abs(x[p * in_dim + j]) > m) { + m = abs(x[p * in_dim + j]); + } + j = j + 1; + } + s4_m[p] = m; + p = p + 1; + } + p = 0; + while (p < CONTEXT_LEN) { + c = 0; + while (c < out_dim) { + const d : f64 = abs(s4_y4[p * out_dim + c] - s4_y3[p * out_dim + c]); + const bound : f64 = s4_m[p] * (s4_nnz[c] / (2.0 * TR4_QMAX) + 0.000000001); + var r : f64 = 0.0; + if (bound > 0.0) { + r = d / bound; + } else if (d != 0.0) { + r = 2.0; + } + if (r > s4_ratio or r != r) { + s4_ratio = r; + } + c = c + 1; + } + p = p + 1; + } +} + +/// The largest error-to-bound ratio of tr4_bound_check over three matmuls of one real window +/// (block 0's K on its normed input, block 1's down on its ReLU output, the head on the normed +/// last block): at most 1 if the bound holds, above 0 only if speed 4 really rounds +fn tr4_kernel_ratio() -> f64 { + tr_tiny_setup(5); + speed_level = 3; + tr3_forward(train_tok[0..ntrain], 700, CONTEXT_LEN, 0); + speed_level = 2; + s4_ratio = 0.0; + const span : usize = CONTEXT_LEN * EMBED_DIM; + const fspan : usize = CONTEXT_LEN * FF_DIM; + tr4_bound_check(b3_n1[0..span], &model.blocks[0].w_k, EMBED_DIM, EMBED_DIM); + tr4_bound_check(b3_h[fspan..2 * fspan], &model.blocks[1].w_down, FF_DIM, EMBED_DIM); + tr4_bound_check(&b3_hn, &model.w_out, EMBED_DIM, VOCAB_SIZE); + return s4_ratio; +} + +/// The logits of all 27 positions of one window at speed 2 and at speed 4: the largest |difference| +/// (s4_err) and the largest |logit| at speed 2 (s4_scale); returns s4_err / s4_scale +fn tr4_logit_relerr() -> f64 { + tr_tiny_setup(5); + speed_level = 4; + tr3_forward(train_tok[0..ntrain], 700, CONTEXT_LEN, 0); + speed_level = 2; + s4_err = 0.0; + s4_scale = 0.0; + var p : usize = 0; + while (p < CONTEXT_LEN) { + hslm_forward(@as(usize, @intCast(train_tok[700 + p])), p, &ref_logits); + var j : usize = 0; + while (j < VOCAB_SIZE) { + const d : f64 = abs(ref_logits[j] - b3_logits[p * VOCAB_SIZE + j]); + if (d > s4_err or d != d) { + s4_err = d; + } + if (abs(ref_logits[j]) > s4_scale) { + s4_scale = abs(ref_logits[j]); + } + j = j + 1; + } + p = p + 1; + } + return s4_err / s4_scale; +} + +/// |k-position loss at speed 4 - at speed 2| of one window, forward only +fn tr4_loss_err(k: usize) -> f64 { + tr_tiny_setup(5); + speed_level = 2; + const l2 : f64 = tr_window_loss_k(train_tok[0..ntrain], 900, k); + speed_level = 4; + const l4 : f64 = tr_window_loss_k(train_tok[0..ntrain], 900, k); + speed_level = 2; + return abs(l4 - l2); +} + +/// ||g4 - g2|| / ||g2|| over all NPARAM gradients (gathered unscaled) of one window of the +/// k-position objective, speed 4 against speed 2 +fn tr4_grad_relerr(k: usize) -> f64 { + tr_tiny_setup(5); + speed_level = 2; + zero_weight_gradients(); + const l2 : f64 = tr_window_grad_k(train_tok[0..ntrain], 700, k); + tr_gather(1.0); + var i : usize = 0; + while (i < NPARAM) { + s3_grad[i] = g_val[i]; + i = i + 1; + } + speed_level = 4; + zero_weight_gradients(); + const l4 : f64 = tr_window_grad_k(train_tok[0..ntrain], 700, k); + tr_gather(1.0); + speed_level = 2; + var num : f64 = 0.0; + var den : f64 = 0.0; + i = 0; + while (i < NPARAM) { + const d : f64 = g_val[i] - s3_grad[i]; + num = num + d * d; + den = den + s3_grad[i] * s3_grad[i]; + i = i + 1; + } + if (den == 0.0 or l2 != l2 or l4 != l4) { + return 1.0; + } + return sqrt(num / den); +} + /// Bytes that differ between an MSLH image and the image packed again from its own decoded floats fn tr_mslh_roundtrip_diff() -> usize { tr_init(3); @@ -3394,6 +3783,46 @@ test speed_three_lab_tok_s_and_its_speed_up_follow_from_the_measured_times and ratio < 0.0005 and again > 1.4 +test speed_four_keeps_every_int8_matmul_output_within_its_proven_bound + given ratio = tr4_kernel_ratio() + then ratio <= 1.0 + and ratio > 0.05 + +test speed_four_logits_stay_within_two_percent_of_speed_two + given rel = tr4_logit_relerr() + then rel < 0.02 + +test speed_four_k_position_loss_stays_within_a_thousandth_of_speed_two + given e27 = tr4_loss_err(27) + and e1 = tr4_loss_err(1) + then e27 < 0.001 + and e1 < 0.001 + and e27 > 0.0 + +test speed_four_gradients_stay_within_five_percent_of_speed_two + given g27 = tr4_grad_relerr(27) + and g1 = tr4_grad_relerr(1) + then g27 < 0.05 + and g1 < 0.05 + and g27 > 0.0 + +test speed_four_lab_loss_curves_stay_within_a_thousandth_of_speed_two_over_300_steps + given vg = tr4_curve_valid_gap() + and tg = tr4_curve_train_gap() + and fall = tr4_curve_fall() + then vg < 0.001 + and vg > 0.0 + and tg < 0.001 + and fall > 0.25 + +test speed_four_lab_tok_s_and_its_speed_ups_follow_from_the_measured_times + given toks = tr4_toks_gap() + and ratio = tr4_ratio_gap() + and again = tr4_speedup_again() + then toks < 0.05 + and ratio < 0.001 + and again > 1.2 + test a_few_steps_on_a_tiny_fixed_slice_lower_the_loss given after = tr_tiny_after() and before = tr_tiny_before() From 2c82da4f5f6cba6644f8459c3adddba686a70bd3 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 15:44:15 +0700 Subject: [PATCH 2/3] seal(ml): restore master's train_hslm seal so the master merge is conflict-free (#10875) The seal is regenerated on the lab in the next commit, after the merge. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/ml_train_hslm.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.trinity/seals/ml_train_hslm.json b/.trinity/seals/ml_train_hslm.json index 53453d7aae..066dc275c6 100644 --- a/.trinity/seals/ml_train_hslm.json +++ b/.trinity/seals/ml_train_hslm.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+78e57be99", + "built_by": "t27c-bootstrap@0.5.2+353cf1df0", "closure": [ { "spec": "specs/base/types.t27", @@ -19,23 +19,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:ceedef84e0e719b3e6c65b490748115e8d9f4202b9bb49dd991389839c3b312c", - "gen_hash_rust": "sha256:c31680740ca123012b226b6df09915b79b59c4117ba598bacaf773d6f400bdd6", - "gen_hash_verilog": "sha256:782d456603a228e656ebb47361f67f29a5ed4696aa613c539ac2fdde2eafcd30", - "gen_hash_zig": "sha256:1c3cf9b8516a7ad1aaaea70758260092783f0d69a5ed61dce754bf8defd5db9d", + "gen_hash_c": "sha256:822f833d6c18b7e4934d3b2d642f2c148fe1f474a913a8d3ae8b5d66a8574e3e", + "gen_hash_rust": "sha256:e51f59949e0422e8c502718227650dc98a439ef672bf30f57cb81845fca2eed9", + "gen_hash_verilog": "sha256:9e721a0fd0edb9a087beff8610b322cf6c08058be2eae63646179ebac438d0b7", + "gen_hash_zig": "sha256:932f799ed156fede9141759f9f8d0f06c23700006b5362756b8e67d4a6d51b5e", "module": "train_hslm", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T06:08:20Z", + "sealed_at": "2026-10-19T05:50:26Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:68983f776ddf327d028cc9957e8ccde6d9200c9a681f3ee9be7b62deca00316d", + "spec_hash": "sha256:84db90108f99fb68a750e82e6cd104f669900631495eafa9ae50574f956e23c4", "spec_path": "specs/ml/train_hslm.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 33, - "total": 33, + "passed": 30, + "total": 30, "vacuous": 0 }, "toolchain": { From a9e20bf4c2a8723c741bbe60370cbdae8e7b530f Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 15:45:57 +0700 Subject: [PATCH 3/3] spec(ml): speed=4 scratch is threadlocal and the speed-4 step is bit-identical at 1 and 3 threads (#10875) Master's thread primitive (#10896) makes window scratch threadlocal; the speed-4 buffers b4_q, b4_s and b4_wt follow. New test checks the step digest at 1 and 3 threads at speed 4, and that it differs from speed 2. Re-sealed on the lab (37 tests, 0 vacuous, all hashes MATCH). Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/ml_train_hslm.json | 18 +++++++++--------- specs/ml/train_hslm.t27 | 14 +++++++++++--- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.trinity/seals/ml_train_hslm.json b/.trinity/seals/ml_train_hslm.json index 066dc275c6..2ea7d462a9 100644 --- a/.trinity/seals/ml_train_hslm.json +++ b/.trinity/seals/ml_train_hslm.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+353cf1df0", + "built_by": "t27c-bootstrap@0.5.2+0d7445c21", "closure": [ { "spec": "specs/base/types.t27", @@ -19,23 +19,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:822f833d6c18b7e4934d3b2d642f2c148fe1f474a913a8d3ae8b5d66a8574e3e", - "gen_hash_rust": "sha256:e51f59949e0422e8c502718227650dc98a439ef672bf30f57cb81845fca2eed9", - "gen_hash_verilog": "sha256:9e721a0fd0edb9a087beff8610b322cf6c08058be2eae63646179ebac438d0b7", - "gen_hash_zig": "sha256:932f799ed156fede9141759f9f8d0f06c23700006b5362756b8e67d4a6d51b5e", + "gen_hash_c": "sha256:5dae40ef9488041b67d533c2896b747c26c204061713088562a7490b71f7d88c", + "gen_hash_rust": "sha256:5f4cf5c98d360713cade40213176a5df9bc58fc6d18b041f82da6854198e7535", + "gen_hash_verilog": "sha256:2085f8329b41b3e291797c5c92bb4ff0af4b63682ce6a9f7d2db0e630bee4cff", + "gen_hash_zig": "sha256:60feb0ba5d9be71497bc4683272bed1e053f3a395f6e4ac7c92e16d9518d729c", "module": "train_hslm", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T05:50:26Z", + "sealed_at": "2026-10-19T08:42:04Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:84db90108f99fb68a750e82e6cd104f669900631495eafa9ae50574f956e23c4", + "spec_hash": "sha256:64c7893d59bb1506e9dac448f07a944529dff50e8e02220f70d18b592028ded3", "spec_path": "specs/ml/train_hslm.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 30, - "total": 30, + "passed": 37, + "total": 37, "vacuous": 0 }, "toolchain": { diff --git a/specs/ml/train_hslm.t27 b/specs/ml/train_hslm.t27 index d39e08e884..be4014cb59 100644 --- a/specs/ml/train_hslm.t27 +++ b/specs/ml/train_hslm.t27 @@ -871,9 +871,9 @@ fn tr3_matmul(x: []const f64, weights: []const Trit, y: []f64, in_dim: usize, ou ; = (max_j |x[p][j]| / 254) nnz_c, nnz_c the nonzero trits in column c; a test checks every output of ; three matmuls of a real window against it. const TR4_QMAX : f64 = 127.0; -var b4_q : [CONTEXT_LEN * FF_DIM]i16 = undefined; -var b4_s : [CONTEXT_LEN]f64 = undefined; -var b4_wt : [FF_DIM * EMBED_DIM]i16 = undefined; +threadlocal var b4_q : [CONTEXT_LEN * FF_DIM]i16 = undefined; +threadlocal var b4_s : [CONTEXT_LEN]f64 = undefined; +threadlocal var b4_wt : [FF_DIM * EMBED_DIM]i16 = undefined; /// y[p] = s_p * (q[p] W) for rows p_lo..p_hi, q[p] the int8 image of x[p] with the one scale s_p; /// W transposed into b4_wt once per call, then one i16 dot product per output in sixteen i32 lane @@ -4423,3 +4423,11 @@ test one_two_and_three_threads_give_one_step_the_same_bits_at_speed_2_and_3 and after - before == 6 and one3 == three3 and one3 == one + +test one_and_three_threads_give_one_step_the_same_bits_at_speed_4 + given one4 = tr_thread_step_digest(1, 4) + and three4 = tr_thread_step_digest(3, 4) + and two = tr_thread_step_digest(1, 2) + then one4 == three4 + and one4 != 0 + and one4 != two