|
| 1 | +// SPDX-Licence-Identifier: EUPL-1.2 |
| 2 | + |
| 3 | +package needle |
| 4 | + |
| 5 | +import ( |
| 6 | + "math" |
| 7 | + "slices" |
| 8 | + "testing" |
| 9 | + |
| 10 | + coreio "dappco.re/go/io" |
| 11 | +) |
| 12 | + |
| 13 | +// snapshotDir is the local Needle checkpoint. Tests that need the real weights |
| 14 | +// skip when it is absent (e.g. CI), so the suite stays green without the model |
| 15 | +// while giving a real end-to-end receipt on a machine that has it. |
| 16 | +const snapshotDir = "/Users/snider/.cache/huggingface/hub/models--Cactus-Compute--needle/snapshots/5f89b4307696d669c3df1d38ae057e6e1728b107" |
| 17 | + |
| 18 | +// oracleQuery/oracleTools are the exact prompt the PyTorch reference was run on; |
| 19 | +// the fixtures below are that run's output (see the lane report). |
| 20 | +const ( |
| 21 | + oracleQuery = "What is the weather in San Francisco?" |
| 22 | + oracleTools = `[{"name":"get_weather","description":"Get current weather for a city.","parameters":{"location":{"type":"string","description":"City name.","required":true}}}]` |
| 23 | +) |
| 24 | + |
| 25 | +var ( |
| 26 | + oracleEncIDs = []int{4279, 743, 302, 1149, 362, 711, 327, 1295, 1075, 378, 275, 8047, 8105, 5, 356, 294, 264, 358, 8062, 1331, 265, 283, 264, 618, 407, 1149, 345, 289, 2082, 284, 318, 282, 506, 282, 298, 264, 315, 265, 283, 264, 2523, 417, 284, 301, 262, 312, 434} |
| 27 | + oracleGenIDs = []int{4, 356, 294, 264, 358, 8062, 1331, 265, 393, 282, 506, 264, 8074, 327, 1295, 1075, 378, 275, 8047, 503} |
| 28 | + oracleOutput = ` [{"name":"get_weather","arguments":{"location":"San Francisco"}}]` |
| 29 | +) |
| 30 | + |
| 31 | +func loadTestModel(t *testing.T) *Model { |
| 32 | + t.Helper() |
| 33 | + if !coreio.Local.Exists(snapshotDir + "/model.safetensors") { |
| 34 | + t.Skip("needle checkpoint not present at " + snapshotDir) |
| 35 | + } |
| 36 | + m, err := Load(snapshotDir) |
| 37 | + if err != nil { |
| 38 | + t.Fatalf("Load: %v", err) |
| 39 | + } |
| 40 | + return m |
| 41 | +} |
| 42 | + |
| 43 | +func close32(a, b, tol float32) bool { return float32(math.Abs(float64(a-b))) <= tol } |
| 44 | + |
| 45 | +// TestModel_Generate_ToolCall is the headline receipt: from the real weights, the |
| 46 | +// reference greedily generates the exact coherent function call the PyTorch |
| 47 | +// oracle produced. |
| 48 | +func TestModel_Generate_ToolCall(t *testing.T) { |
| 49 | + m := loadTestModel(t) |
| 50 | + got := m.Generate(oracleQuery, oracleTools, 64) |
| 51 | + if got != oracleOutput { |
| 52 | + t.Fatalf("Generate mismatch:\n got: %q\nwant: %q", got, oracleOutput) |
| 53 | + } |
| 54 | + t.Logf("VERBATIM GENERATION: %s", got) |
| 55 | +} |
| 56 | + |
| 57 | +// TestModel_generateIDs_TokenParity pins exact encoder-input and generated token |
| 58 | +// ids against the oracle — a stricter check than the decoded string. |
| 59 | +func TestModel_generateIDs_TokenParity(t *testing.T) { |
| 60 | + m := loadTestModel(t) |
| 61 | + enc, gen := m.generateIDs(oracleQuery, oracleTools, 64) |
| 62 | + if !slices.Equal(enc, oracleEncIDs) { |
| 63 | + t.Fatalf("encoder ids mismatch:\n got: %v\nwant: %v", enc, oracleEncIDs) |
| 64 | + } |
| 65 | + if !slices.Equal(gen, oracleGenIDs) { |
| 66 | + t.Fatalf("generated ids mismatch:\n got: %v\nwant: %v", gen, oracleGenIDs) |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +// TestModel_encode_EncoderHidden checks the encoder pass numerically against the |
| 71 | +// oracle's encoder output (first token, last token, and the whole-tensor sum), |
| 72 | +// isolating encoder correctness from the decoder. |
| 73 | +func TestModel_encode_EncoderHidden(t *testing.T) { |
| 74 | + m := loadTestModel(t) |
| 75 | + h := m.encode(oracleEncIDs) |
| 76 | + if len(h) != len(oracleEncIDs)*m.cfg.HiddenSize { |
| 77 | + t.Fatalf("encoder hidden len = %d, want %d", len(h), len(oracleEncIDs)*m.cfg.HiddenSize) |
| 78 | + } |
| 79 | + wantT0 := []float32{-1.44818, 0.02975, 0.43704, 0.51372, -1.47645} |
| 80 | + wantTlast := []float32{-0.62455, 0.66252, 0.34196, 0.43071, 0.47612} |
| 81 | + hidden := m.cfg.HiddenSize |
| 82 | + last := (len(oracleEncIDs) - 1) * hidden |
| 83 | + for i := range wantT0 { |
| 84 | + if !close32(h[i], wantT0[i], 0.02) { |
| 85 | + t.Errorf("enc_hid[0][%d] = %.5f, want %.5f", i, h[i], wantT0[i]) |
| 86 | + } |
| 87 | + if !close32(h[last+i], wantTlast[i], 0.02) { |
| 88 | + t.Errorf("enc_hid[last][%d] = %.5f, want %.5f", i, h[last+i], wantTlast[i]) |
| 89 | + } |
| 90 | + } |
| 91 | + var sum float32 |
| 92 | + for _, v := range h { |
| 93 | + sum += v |
| 94 | + } |
| 95 | + if !close32(sum, -730.317, 2.0) { |
| 96 | + t.Errorf("encoder hidden sum = %.3f, want ~-730.317", sum) |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +// TestModel_decode_CrossAttentionStep checks the decoder-with-cross-attention |
| 101 | +// step: the decoder hidden (post-norm) after one step over [EOS], against the |
| 102 | +// oracle. This exercises masked self-attn + cross-attn + gates end to end. |
| 103 | +func TestModel_decode_CrossAttentionStep(t *testing.T) { |
| 104 | + m := loadTestModel(t) |
| 105 | + encoderHidden := m.encode(oracleEncIDs) |
| 106 | + dh := m.decode([]int{m.cfg.EosTokenID}, encoderHidden, len(oracleEncIDs)) |
| 107 | + want := []float32{-0.01168, 0.01785, -0.14802, -0.12168, 0.00175} |
| 108 | + for i := range want { |
| 109 | + if !close32(dh[i], want[i], 0.02) { |
| 110 | + t.Errorf("dec_hid[step0][%d] = %.5f, want %.5f", i, dh[i], want[i]) |
| 111 | + } |
| 112 | + } |
| 113 | + // First-token argmax must be <tool_call> (id 4). |
| 114 | + if got := argmax(m.logits(dh[:m.cfg.HiddenSize])); got != 4 { |
| 115 | + t.Errorf("first-token argmax = %d, want 4 (<tool_call>)", got) |
| 116 | + } |
| 117 | +} |
0 commit comments