|
15 | 15 |
|
16 | 16 | """Tests for MUSA MatMul operator.""" |
17 | 17 |
|
| 18 | +import os |
| 19 | +os.environ.setdefault("MUSA_ENABLE_TF32", "0") |
| 20 | + |
18 | 21 | import tensorflow as tf |
19 | 22 | import numpy as np |
20 | 23 |
|
21 | 24 | from musa_test_utils import MUSATestCase |
22 | 25 |
|
23 | 26 |
|
| 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 | + |
24 | 41 | class MatMulOpTest(MUSATestCase): |
25 | 42 | """Tests for MUSA MatMul operator, TF32 enabled by default.""" |
26 | 43 |
|
@@ -233,6 +250,109 @@ def testBatchMatMulForwardEmptyInnerDim(self): |
233 | 250 | atol=0, |
234 | 251 | ) |
235 | 252 |
|
| 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 | + |
236 | 356 |
|
237 | 357 | if __name__ == "__main__": |
238 | 358 | tf.test.main() |
0 commit comments