Skip to content

Commit 0cf5d12

Browse files
Fix(BatchMatMulBroadcast3Dx2D) (#281)
1 parent 5d560b2 commit 0cf5d12

2 files changed

Lines changed: 125 additions & 2 deletions

File tree

musa_ext/kernels/math/musa_matmul_op.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,16 @@ class MusaMatMulOp : public MusaOpKernel {
143143

144144
if (dims != 3) {
145145
if (batch == 1 && out_batch > 1) {
146-
mt.SetNdInfo({out_batch, rows, cols}, {0, cols, 1});
146+
// muDNN BatchMatMul broadcast: batch_b must be 1, stride_b=0.
147+
// Setting shape as {out_batch, ...} with stride=0 causes
148+
// INVALID_PARAMETER; use {1, ...} to match the API contract.
149+
mt.SetNdInfo({1, rows, cols}, {0, cols, 1});
147150
} else {
148151
mt.SetNdInfo({batch, rows, cols}, {rows * cols, cols, 1});
149152
}
150153
} else if (dims == 3) {
151154
if (batch == 1 && out_batch > 1) {
152-
mt.SetNdInfo({out_batch, rows, cols}, {0, cols, 1});
155+
mt.SetNdInfo({1, rows, cols}, {0, cols, 1});
153156
}
154157
}
155158
};

test/ops/matmul_op_test.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,29 @@
1515

1616
"""Tests for MUSA MatMul operator."""
1717

18+
import os
19+
os.environ.setdefault("MUSA_ENABLE_TF32", "0")
20+
1821
import tensorflow as tf
1922
import numpy as np
2023

2124
from musa_test_utils import MUSATestCase
2225

2326

27+
def is_tf32_enabled():
28+
return int(os.environ.get("MUSA_ENABLE_TF32", "0")) != 0
29+
30+
31+
def float32_tolerance(default_rtol=1e-3, default_atol=1e-3):
32+
"""Return (rtol, atol) appropriate for float32 matmul comparisons.
33+
34+
When TF32 is enabled (MUSA_ENABLE_TF32=1), MUSA uses 10-bit mantissa
35+
precision internally, so errors can reach ~1e-2 for large matrices.
36+
When TF32 is disabled (default), full FP32 precision applies.
37+
"""
38+
return (1e-2, 1e-2) if is_tf32_enabled() else (default_rtol, default_atol)
39+
40+
2441
class MatMulOpTest(MUSATestCase):
2542
"""Tests for MUSA MatMul operator, TF32 enabled by default."""
2643

@@ -233,6 +250,109 @@ def testBatchMatMulForwardEmptyInnerDim(self):
233250
atol=0,
234251
)
235252

253+
def testBatchMatMulBroadcast3Dx2D(self):
254+
"""3-D × 2-D broadcast BatchMatMul: in1 is a shared weight matrix.
255+
256+
Regression test for INVALID_PARAMETER (Status: 1) from mBatchMatMul when
257+
the 2-D input is broadcast across the batch dimension.
258+
259+
Root cause: ReshapeTo3D set shape={out_batch, rows, cols} with stride={0,...}
260+
for the 2-D input, but muDNN requires batch_b=1 when stride_b=0.
261+
Fix: use shape={1, rows, cols} for the broadcast case.
262+
263+
This pattern is triggered by AFM's AttentionPooling:
264+
e = tf.matmul(interactions, self.W) # (bs, num_pairs, k) x (k, t)
265+
e = tf.matmul(e, self.h) # (bs, num_pairs, t) x (t, 1)
266+
267+
Note on tolerance: MUSA defaults to TF32 (MUSA_ENABLE_TF32=1), which has
268+
FP16-level mantissa precision (10 bits). For large k (e.g. k=256), errors
269+
accumulate to ~1e-2, so float32 on MUSA requires the same tolerance as
270+
float16 rather than the full-precision 1e-3.
271+
272+
For float16, only small-k shapes are tested: fp16 accumulation error scales
273+
as k × eps_fp16 ≈ k × 1e-3, which exceeds 1e-2 at k=256. The broadcast
274+
fix is dtype-agnostic, so small shapes are sufficient to cover the path.
275+
"""
276+
# float32: test both small and large k (large k validates TF32 tolerance).
277+
# float16: small k only — fp16 precision with k=256 exceeds rtol=1e-2.
278+
test_shapes = {
279+
tf.float32: {
280+
"case1": [(4, 10, 16, 8), (8, 45, 256, 64)],
281+
"case2": [(4, 10, 8), (8, 45, 64)],
282+
},
283+
tf.float16: {
284+
"case1": [(4, 10, 16, 8)],
285+
"case2": [(4, 10, 8)],
286+
},
287+
}
288+
289+
for dtype in [tf.float32, tf.float16]:
290+
rtol, atol = (1e-2, 1e-2) if dtype == tf.float16 else float32_tolerance()
291+
np_dtype = np.float16 if dtype == tf.float16 else np.float32
292+
293+
# Case 1: (bs, num_pairs, k) x (k, t) — attention W matrix
294+
for bs, num_pairs, k, t in test_shapes[dtype]["case1"]:
295+
a_np = np.random.uniform(-1, 1, (bs, num_pairs, k)).astype(np_dtype)
296+
b_np = np.random.uniform(-1, 1, (k, t)).astype(np_dtype)
297+
a = tf.constant(a_np, dtype=dtype)
298+
b = tf.constant(b_np, dtype=dtype)
299+
300+
with tf.device('/CPU:0'):
301+
cpu_result = tf.cast(tf.matmul(a, b), tf.float32)
302+
with tf.device('/device:MUSA:0'):
303+
musa_result = tf.cast(tf.matmul(a, b), tf.float32)
304+
305+
self.assertAllClose(musa_result.numpy(), cpu_result.numpy(),
306+
rtol=rtol, atol=atol)
307+
308+
# Case 2: (bs, num_pairs, t) x (t, 1) — attention h vector (n=1)
309+
for bs, num_pairs, t in test_shapes[dtype]["case2"]:
310+
a_np = np.random.uniform(-1, 1, (bs, num_pairs, t)).astype(np_dtype)
311+
b_np = np.random.uniform(-1, 1, (t, 1)).astype(np_dtype)
312+
a = tf.constant(a_np, dtype=dtype)
313+
b = tf.constant(b_np, dtype=dtype)
314+
315+
with tf.device('/CPU:0'):
316+
cpu_result = tf.cast(tf.matmul(a, b), tf.float32)
317+
with tf.device('/device:MUSA:0'):
318+
musa_result = tf.cast(tf.matmul(a, b), tf.float32)
319+
320+
self.assertAllClose(musa_result.numpy(), cpu_result.numpy(),
321+
rtol=rtol, atol=atol)
322+
323+
def testBatchMatMulBroadcast3Dx2DGrad(self):
324+
"""Backward pass of 3-D × 2-D broadcast BatchMatMul.
325+
326+
Verifies that gradients flow correctly through both weight matrices
327+
in the AFM AttentionPooling pattern during training.
328+
"""
329+
bs, num_pairs, k, t = 4, 10, 16, 8
330+
331+
a_np = np.random.uniform(-1, 1, (bs, num_pairs, k)).astype(np.float32)
332+
W_np = np.random.uniform(-1, 1, (k, t)).astype(np.float32)
333+
h_np = np.random.uniform(-1, 1, (t, 1)).astype(np.float32)
334+
335+
def run_attention_pooling(device):
336+
with tf.device(device):
337+
interactions = tf.constant(a_np)
338+
W = tf.Variable(W_np)
339+
h = tf.Variable(h_np)
340+
with tf.GradientTape() as tape:
341+
e = tf.nn.relu(tf.matmul(interactions, W)) # (bs, num_pairs, t)
342+
e = tf.matmul(e, h) # (bs, num_pairs, 1)
343+
loss = tf.reduce_sum(e)
344+
grads = tape.gradient(loss, [W, h])
345+
return grads
346+
347+
cpu_grads = run_attention_pooling('/CPU:0')
348+
musa_grads = run_attention_pooling('/device:MUSA:0')
349+
350+
rtol, atol = float32_tolerance()
351+
self.assertAllClose(musa_grads[0].numpy(), cpu_grads[0].numpy(),
352+
rtol=rtol, atol=atol)
353+
self.assertAllClose(musa_grads[1].numpy(), cpu_grads[1].numpy(),
354+
rtol=rtol, atol=atol)
355+
236356

237357
if __name__ == "__main__":
238358
tf.test.main()

0 commit comments

Comments
 (0)