|
| 1 | +// Copyright 2023-2026 The GoMLX Authors. SPDX-License-Identifier: Apache-2.0 |
| 2 | + |
| 3 | +package xla |
| 4 | + |
| 5 | +import ( |
| 6 | + "math" |
| 7 | + "testing" |
| 8 | + |
| 9 | + "github.com/gomlx/compute" |
| 10 | + "github.com/gomlx/compute/dtypes" |
| 11 | + "github.com/gomlx/compute/dtypes/bfloat16" |
| 12 | + "github.com/gomlx/compute/shapes" |
| 13 | +) |
| 14 | + |
| 15 | +// getFusionBackend creates a cuda backend and probes FusedScaledDotProductAttention on a tiny |
| 16 | +// causal bf16 input. Skips if the cuda plugin is unavailable or if cuDNN fMHA is not supported. |
| 17 | +func getFusionBackend(t *testing.T) *Backend { |
| 18 | + t.Helper() |
| 19 | + be, err := NewWithOptions("cuda", nil) |
| 20 | + if err != nil { |
| 21 | + t.Skipf("[cuda] plugin unavailable: %v", err) |
| 22 | + } |
| 23 | + t.Cleanup(func() { be.Finalize() }) |
| 24 | + |
| 25 | + // Probe: tiny causal bf16 forward to confirm cuDNN fMHA actually works on this host. |
| 26 | + const B, S, H, D = 1, 4, 2, 8 |
| 27 | + bld := be.Builder("fmha_probe").(*Builder) |
| 28 | + fn := bld.Main().(*Function) |
| 29 | + qkvShape := shapes.Make(dtypes.BFloat16, B, S, H, D) |
| 30 | + q, err := fn.Parameter("q", qkvShape, nil) |
| 31 | + if err != nil { |
| 32 | + t.Fatalf("probe: parameter q: %v", err) |
| 33 | + } |
| 34 | + k, err := fn.Parameter("k", qkvShape, nil) |
| 35 | + if err != nil { |
| 36 | + t.Fatalf("probe: parameter k: %v", err) |
| 37 | + } |
| 38 | + v, err := fn.Parameter("v", qkvShape, nil) |
| 39 | + if err != nil { |
| 40 | + t.Fatalf("probe: parameter v: %v", err) |
| 41 | + } |
| 42 | + out, _, err := fn.FusedScaledDotProductAttention(q, k, v, nil, H, H, |
| 43 | + compute.AxesLayoutBSHD, 1.0/math.Sqrt(float64(D)), true, nil) |
| 44 | + if compute.IsNotImplemented(err) { |
| 45 | + t.Skipf("[cuda] cuDNN fMHA not supported on this host: %v", err) |
| 46 | + } |
| 47 | + if err != nil { |
| 48 | + t.Fatalf("probe: FusedScaledDotProductAttention: %v", err) |
| 49 | + } |
| 50 | + if err = fn.Return([]compute.Value{out}, nil); err != nil { |
| 51 | + t.Fatalf("probe: Return: %v", err) |
| 52 | + } |
| 53 | + if _, err = bld.Compile(); err != nil { |
| 54 | + t.Skipf("[cuda] cuDNN fMHA compile failed (unsupported cuDNN): %v", err) |
| 55 | + } |
| 56 | + return be |
| 57 | +} |
| 58 | + |
| 59 | +// assertFiniteBSHD reads buf (bf16) and fails if any element is NaN or Inf. |
| 60 | +func assertFiniteBSHD(t *testing.T, buf compute.Buffer) { |
| 61 | + t.Helper() |
| 62 | + sh, err := buf.Shape() |
| 63 | + if err != nil { |
| 64 | + t.Fatalf("assertFiniteBSHD Shape: %v", err) |
| 65 | + } |
| 66 | + flat := make([]bfloat16.BFloat16, sh.Size()) |
| 67 | + if err = buf.ToFlatData(flat); err != nil { |
| 68 | + t.Fatalf("assertFiniteBSHD ToFlatData: %v", err) |
| 69 | + } |
| 70 | + bad := 0 |
| 71 | + for _, x := range flat { |
| 72 | + f := x.Float32() |
| 73 | + if math.IsNaN(float64(f)) || math.IsInf(float64(f), 0) { |
| 74 | + bad++ |
| 75 | + } |
| 76 | + } |
| 77 | + if bad > 0 { |
| 78 | + t.Errorf("assertFiniteBSHD: %d/%d non-finite values", bad, len(flat)) |
| 79 | + } else { |
| 80 | + t.Logf("assertFiniteBSHD: all %d outputs finite", len(flat)) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +// readFlatBF16 extracts the flat bf16 slice from buf, failing the test on error. |
| 85 | +func readFlatBF16(t *testing.T, buf compute.Buffer) ([]bfloat16.BFloat16, shapes.Shape) { |
| 86 | + t.Helper() |
| 87 | + sh, err := buf.Shape() |
| 88 | + if err != nil { |
| 89 | + t.Fatalf("readFlatBF16 Shape: %v", err) |
| 90 | + } |
| 91 | + flat := make([]bfloat16.BFloat16, sh.Size()) |
| 92 | + if err = buf.ToFlatData(flat); err != nil { |
| 93 | + t.Fatalf("readFlatBF16 ToFlatData: %v", err) |
| 94 | + } |
| 95 | + return flat, sh |
| 96 | +} |
| 97 | + |
| 98 | +// assertSeqLenMasksOutput verifies that seqlen masking actually changed the output. It compares |
| 99 | +// the padding positions (seqIdx >= shortLen) of batch element 1 (masked to shortLen) against the |
| 100 | +// same positions of batch element 0 (full seqLen). With q=k=v=ones and PADDING_CAUSAL, cuDNN |
| 101 | +// zeros/masks the padding-query outputs for the short batch element, producing values that differ |
| 102 | +// from the active (non-zero) outputs of the full batch element. If seqlens were ignored, all |
| 103 | +// positions across both elements would be identical and the assertion would fail. |
| 104 | +// |
| 105 | +// Layout: flat [B, S, H, D] row-major. shortLen < seqLen required. |
| 106 | +func assertSeqLenMasksOutput(t *testing.T, flat []bfloat16.BFloat16, b, seqLen, h, d, shortLen int) { |
| 107 | + t.Helper() |
| 108 | + if shortLen >= seqLen { |
| 109 | + t.Fatalf("assertSeqLenMasksOutput: shortLen %d must be < seqLen %d", shortLen, seqLen) |
| 110 | + } |
| 111 | + stride := func(batch, seq int) int { return (batch*seqLen+seq)*h*d } |
| 112 | + // Count positions in [batch=1, s=shortLen..seqLen-1] that differ from the corresponding |
| 113 | + // position in [batch=0] (which has full seqlen and should be active/non-zero). |
| 114 | + diffCount := 0 |
| 115 | + totalPad := (seqLen - shortLen) * h * d |
| 116 | + for s := shortLen; s < seqLen; s++ { |
| 117 | + base0 := stride(0, s) |
| 118 | + base1 := stride(1, s) |
| 119 | + for i := 0; i < h*d; i++ { |
| 120 | + if flat[base0+i] != flat[base1+i] { |
| 121 | + diffCount++ |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + if diffCount == 0 { |
| 126 | + t.Errorf("assertSeqLenMasksOutput: padding positions (s>=%d) in batch 1 are identical to batch 0 (%d elements) -- seqlen masking had no effect", shortLen, totalPad) |
| 127 | + } else { |
| 128 | + t.Logf("assertSeqLenMasksOutput: %d/%d padding-position elements differ (masking active)", diffCount, totalPad) |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +// [cuda] PADDING_CAUSAL: per-batch lengths shorter than S must mask the padding rows. With |
| 133 | +// q=k=v=ones, masking changes which keys contribute, so the masked output differs from the |
| 134 | +// unmasked all-ones output for the shortened batch element. Runs under xla:cuda. |
| 135 | +func TestFMHA_SeqLenPaddingCausal_cuda(t *testing.T) { |
| 136 | + be := getFusionBackend(t) |
| 137 | + const B, S, H, D = 2, 8, 12, 64 |
| 138 | + scale := 1.0 / math.Sqrt(float64(D)) |
| 139 | + qkvShape := shapes.Make(dtypes.BFloat16, B, S, H, D) |
| 140 | + |
| 141 | + bld := be.Builder("fmha_seqlen").(*Builder) |
| 142 | + fn := bld.Main().(*Function) |
| 143 | + |
| 144 | + q, err := fn.Parameter("q", qkvShape, nil) |
| 145 | + if err != nil { |
| 146 | + t.Fatalf("parameter q: %v", err) |
| 147 | + } |
| 148 | + k, err := fn.Parameter("k", qkvShape, nil) |
| 149 | + if err != nil { |
| 150 | + t.Fatalf("parameter k: %v", err) |
| 151 | + } |
| 152 | + v, err := fn.Parameter("v", qkvShape, nil) |
| 153 | + if err != nil { |
| 154 | + t.Fatalf("parameter v: %v", err) |
| 155 | + } |
| 156 | + // Seqlen constants embedded in the graph: batch 0 uses full S, batch 1 is padded to S/2. |
| 157 | + qSeq, err := fn.Constant([]int32{S, S / 2}, B) |
| 158 | + if err != nil { |
| 159 | + t.Fatalf("constant qSeqLen: %v", err) |
| 160 | + } |
| 161 | + kvSeq, err := fn.Constant([]int32{S, S / 2}, B) |
| 162 | + if err != nil { |
| 163 | + t.Fatalf("constant kvSeqLen: %v", err) |
| 164 | + } |
| 165 | + cfg := &compute.ScaledDotProductAttentionConfig{ |
| 166 | + QuerySeqLen: qSeq, |
| 167 | + KeyValueSeqLen: kvSeq, |
| 168 | + } |
| 169 | + out, _, err := fn.FusedScaledDotProductAttention(q, k, v, nil, H, H, |
| 170 | + compute.AxesLayoutBSHD, scale, true, cfg) |
| 171 | + if err != nil { |
| 172 | + t.Fatalf("FusedScaledDotProductAttention: %v", err) |
| 173 | + } |
| 174 | + if err = fn.Return([]compute.Value{out}, nil); err != nil { |
| 175 | + t.Fatalf("Return: %v", err) |
| 176 | + } |
| 177 | + exec, err := bld.Compile() |
| 178 | + if err != nil { |
| 179 | + t.Fatalf("Compile: %v", err) |
| 180 | + } |
| 181 | + |
| 182 | + nElems := B * S * H * D |
| 183 | + ones := make([]bfloat16.BFloat16, nElems) |
| 184 | + one := bfloat16.FromFloat32(1) |
| 185 | + for i := range ones { |
| 186 | + ones[i] = one |
| 187 | + } |
| 188 | + mkBuf := func() compute.Buffer { |
| 189 | + buf, e := be.BufferFromFlatData(0, ones, qkvShape) |
| 190 | + if e != nil { |
| 191 | + t.Fatalf("BufferFromFlatData: %v", e) |
| 192 | + } |
| 193 | + return buf |
| 194 | + } |
| 195 | + qb, kb, vb := mkBuf(), mkBuf(), mkBuf() |
| 196 | + defer func() { |
| 197 | + _ = qb.Finalize() |
| 198 | + _ = kb.Finalize() |
| 199 | + _ = vb.Finalize() |
| 200 | + }() |
| 201 | + |
| 202 | + outs, err := exec.Execute([]compute.Buffer{qb, kb, vb}, nil, 0) |
| 203 | + if err != nil { |
| 204 | + t.Fatalf("Execute: %v", err) |
| 205 | + } |
| 206 | + defer func() { |
| 207 | + for _, o := range outs { |
| 208 | + _ = o.Finalize() |
| 209 | + } |
| 210 | + }() |
| 211 | + |
| 212 | + // Original finiteness check: no NaN/Inf anywhere in the output. |
| 213 | + assertFiniteBSHD(t, outs[0]) |
| 214 | + |
| 215 | + // Masking-effect check: seqlen masking must change the output for the short batch element. |
| 216 | + // Batch 0 has qSeqLen=S (all active); batch 1 has qSeqLen=S/2 (padding at s>=S/2). |
| 217 | + // cuDNN PADDING_CAUSAL zeros/masks query positions >= qSeqLen, so batch 1's padding |
| 218 | + // positions must differ from batch 0's (which are fully active and non-zero). If cuDNN |
| 219 | + // ignored the seqlens, both elements would produce identical all-ones outputs and this |
| 220 | + // assertion would fail. |
| 221 | + flat, _ := readFlatBF16(t, outs[0]) |
| 222 | + assertSeqLenMasksOutput(t, flat, B, S, H, D, S/2) |
| 223 | +} |
0 commit comments