From 9605415404595af2e5acae232971b46bb7908e29 Mon Sep 17 00:00:00 2001 From: dylanyunlon Date: Fri, 7 Aug 2026 03:13:24 +0000 Subject: [PATCH] test: add reduce tuning verification against CCCL ground truth Tests scale_mem_bound CCCL parity (8/8), register pressure for all 14 bi100_* structs, summary_statistics.cu 28-byte AccumT safety, and vectorization alignment. All pass. Key finding: BI-V100 float32 tile is 1.5x SM100's (12288 vs 8192) because 16 SMs need larger tiles to compensate for fewer CTAs. float64 tile is 0.6x SM100's (6144 vs 10240) because threads=640 was reduced to 384 (clean warp count) and vec=2 added. --- muh/tests/test_reduce_tuning.py | 254 ++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 muh/tests/test_reduce_tuning.py diff --git a/muh/tests/test_reduce_tuning.py b/muh/tests/test_reduce_tuning.py new file mode 100644 index 00000000..7fea7ea1 --- /dev/null +++ b/muh/tests/test_reduce_tuning.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""test_reduce_tuning.py — Verify muh reduce tuning against CCCL ground truth + +Tests that muh's scale_mem_bound + bi100_* struct values produce valid +configurations for all data types used in vllm/Qwen3.6. + +CCCL reference: summary_statistics.cu + AccumT = summary_stats_data (7 floats = 28 bytes) + This is the WORST CASE for SMEM/register pressure testing — + if our tuning handles 28-byte AccumT without overflow, + the common 4-byte (float32) and 2-byte (float16) paths are safe. + +Validation approach: + For each bi100_* struct, compute scale_mem_bound output and verify: + 1. threads × items fits register file (< 64K regs/SM) + 2. For SMEM-using algorithms (scan, select_if): tile ≤ 48KB + 3. For register-only algorithms (reduce): occupancy ≥ 1 CTA/SM + 4. vec_size divides items_per_thread (vectorization alignment) +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +# BI-V100 hardware constants (from hardware.cuh) +SM_COUNT = 16 +SMEM_PER_BLOCK = 49152 # 48KB +REGS_PER_SM = 65536 +MAX_THREADS = 1024 +WARP_SIZE = 32 + +def scale_mem_bound(nominal_threads, nominal_items, type_size, max_smem=SMEM_PER_BLOCK): + """Python mirror of muh::tuning::scale_mem_bound (common.cuh).""" + items = nominal_items * 4 // type_size + items = max(1, min(items, nominal_items * 2)) + smem_per_thread = type_size * items + if smem_per_thread > 0: + raw = max_smem // smem_per_thread + max_threads_by_smem = ((raw + 31) // 32) * 32 + else: + max_threads_by_smem = nominal_threads + threads = min(nominal_threads, max_threads_by_smem) + if threads < 32: + threads = 32 + return items, threads + +# CCCL types used in vllm/Qwen3.6 +TYPE_SIZES = { + "int8": 1, + "float16": 2, + "bfloat16": 2, + "float32": 4, + "float64": 8, + "int64": 8, + "int128": 16, + "summary_stats_float": 28, # 7×float from summary_statistics.cu +} + +# bi100_* structs from tuning_reduce.cuh +BI100_STRUCTS = { + "bi100_plus_accum1_o4": {"threads": 512, "items": 32, "vec": 4}, + "bi100_plus_accum2_o4": {"threads": 512, "items": 24, "vec": 2}, + "bi100_plus_float32_o4": {"threads": 512, "items": 24, "vec": 2}, + "bi100_plus_float32_o8": {"threads": 512, "items": 24, "vec": 1}, + "bi100_plus_float64_o4": {"threads": 384, "items": 16, "vec": 2}, + "bi100_plus_float64_o8": {"threads": 384, "items": 16, "vec": 1}, + "bi100_plus_int64_o4": {"threads": 384, "items": 16, "vec": 2}, + "bi100_plus_int64_o8": {"threads": 384, "items": 16, "vec": 1}, + "bi100_plus_accum16_o4": {"threads": 192, "items": 16, "vec": 1}, + "bi100_det_float32": {"threads": 384, "items": 32, "vec": 1}, + "bi100_det_float64": {"threads": 384, "items": 16, "vec": 1}, + "bi100_det_int32": {"threads": 384, "items": 32, "vec": 1}, + "bi100_det_int16": {"threads": 384, "items": 64, "vec": 1}, + "bi100_default": {"threads": 256, "items": 24, "vec": 4}, +} + + +def test_scale_mem_bound_cccl_parity(): + """Test scale_mem_bound matches CCCL behavior for all type sizes.""" + print("=== scale_mem_bound CCCL parity ===") + + # CCCL test vectors from catch2_test_util_arch.cu MemBoundScaling tests + test_cases = [ + # (nominal_threads, nominal_items, type_size) → expected (items, threads) + (256, 16, 4, 16, 256), # 4B identity + (256, 16, 1, 32, 256), # 1B: items scale up to 2×nominal + (256, 16, 2, 32, 256), # 2B: items scale up + (256, 16, 8, 8, 256), # 8B: items halve + (256, 16, 16, 4, 256), # 16B: items quarter + (512, 16, 4, 16, 512), # larger threads, 4B + (640, 16, 8, 8, 640), # CCCL SM100 float64: no SMEM cap needed + (1024, 16, 16, 4, 768), # 16B: SMEM cap triggers (1024 > 768) + ] + + passed = 0 + for nom_t, nom_i, ts, exp_i, exp_t in test_cases: + got_i, got_t = scale_mem_bound(nom_t, nom_i, ts) + ok = got_i == exp_i and got_t == exp_t + status = "PASS" if ok else "FAIL" + if not ok: + print(f" {status}: scale_mem_bound({nom_t}, {nom_i}, {ts}) = " + f"({got_i}, {got_t}), expected ({exp_i}, {exp_t})") + passed += ok + + print(f" {passed}/{len(test_cases)} passed") + return passed == len(test_cases) + + +def test_reduce_register_pressure(): + """Test that bi100_* reduce structs don't exceed register file.""" + print("\n=== reduce register pressure ===") + + all_ok = True + for name, cfg in BI100_STRUCTS.items(): + threads = cfg["threads"] + items = cfg["items"] + + # Estimate regs per thread: items × (accum_size/4) + ~16 overhead + # For reduce, AccumT is held in registers (not SMEM staging) + # Guess accum_size from struct name + if "accum1" in name or "int8" in name: + accum_size = 1 + elif "accum2" in name or "int16" in name or "det_int16" in name: + accum_size = 2 + elif "float64" in name or "int64" in name: + accum_size = 8 + elif "accum16" in name: + accum_size = 16 + else: + accum_size = 4 # float32 default + + regs_for_data = items * max(1, accum_size // 4) + regs_overhead = 16 # control flow, addresses, etc + regs_per_thread = regs_for_data + regs_overhead + regs_per_cta = threads * regs_per_thread + max_ctas = REGS_PER_SM // regs_per_cta if regs_per_cta > 0 else 0 + + ok = max_ctas >= 1 + status = "PASS" if ok else "FAIL" + if not ok: + all_ok = False + print(f" {status}: {name:30s} threads={threads:4d} items={items:3d} " + f"accum={accum_size:2d}B regs/thread={regs_per_thread:3d} " + f"max_CTAs/SM={max_ctas}") + + return all_ok + + +def test_summary_statistics_accum(): + """Test scale_mem_bound for summary_stats_data (28 bytes). + + This is the Welford parallel stats accumulator from summary_statistics.cu. + If reduce tuning handles this, it handles anything vllm throws at it. + """ + print("\n=== summary_statistics AccumT (28 bytes) ===") + + # Apply CCCL default (SM60 level): threads=256, items=16 + items, threads = scale_mem_bound(256, 16, 28) + print(f" scale_mem_bound(256, 16, 28) = items={items}, threads={threads}") + print(f" Data per thread: {items} × 28B = {items*28}B") + + # Check: items should be small (28B is huge) + # 16 * 4 / 28 = 2.28 → clamp to 2 + assert items == 2, f"Expected items=2 for 28B type, got {items}" + + # SMEM cap: threads = min(256, round_up(49152/(28*2), 32)) + # = min(256, round_up(878, 32)) = min(256, 896) = 256 + assert threads == 256, f"Expected threads=256 for 28B type, got {threads}" + + # Register check: 2 items × 7 floats × 4B = 56B → 14 regs → trivial + regs = items * 7 # 7 float fields in summary_stats_data + print(f" Registers for data: {regs} (14 regs per thread) — well within 255 limit") + print(f" PASS: summary_statistics AccumT is safe on BI-V100") + return True + + +def test_vec_alignment(): + """Test that vec_size divides items_per_thread for all structs.""" + print("\n=== vectorization alignment ===") + + all_ok = True + for name, cfg in BI100_STRUCTS.items(): + vec = cfg.get("vec", 1) + items = cfg["items"] + if vec > 1: + ok = items % vec == 0 + if not ok: + all_ok = False + print(f" FAIL: {name} items={items} not divisible by vec={vec}") + + if all_ok: + print(f" PASS: all {len(BI100_STRUCTS)} structs have valid vec alignment") + return all_ok + + +def test_cccl_sm100_comparison(): + """Compare muh bi100 values against CCCL SM100 tuning comments.""" + print("\n=== CCCL SM100 comparison ===") + + # From tuning_reduce.cuh SM100 benchmark comments: + sm100 = { + "float32_o4": {"items": 16, "threads": 512, "vec": 2, + "speedup": [1.061295, 1.000000, 1.065478, 1.167139]}, + "float64_o4": {"items": 16, "threads": 640, "vec": 1, + "speedup": [1.017834, 1.000000, 1.015835, 1.057092]}, + "8B_o4": {"items": 15, "threads": 512, "vec": 2, + "speedup": [1.019887, 1.0, 1.017636, 1.058036]}, + "8B_o8": {"items": 15, "threads": 512, "vec": 1, + "speedup": [1.019414, 1.000000, 1.017218, 1.057143]}, + } + + bi100 = { + "float32_o4": BI100_STRUCTS["bi100_plus_float32_o4"], + "float64_o4": BI100_STRUCTS["bi100_plus_float64_o4"], + } + + for key in ["float32_o4", "float64_o4"]: + s = sm100[key] + b = bi100[key] + tile_ratio = (b["threads"] * b["items"]) / (s["threads"] * s["items"]) + print(f" {key}:") + print(f" SM100: threads={s['threads']:4d} items={s['items']:2d} " + f"vec={s['vec']} tile={s['threads']*s['items']:6d}") + print(f" BI100: threads={b['threads']:4d} items={b['items']:2d} " + f"vec={b['vec']} tile={b['threads']*b['items']:6d} " + f"(ratio={tile_ratio:.2f}×)") + print(f" SM100 speedup: {' '.join(f'{x:.3f}' for x in s['speedup'])}") + print(f" BI100 speedup: [TBD — needs real benchmark on Phanthy Cloud]") + + +def main(): + print("muh reduce tuning verification") + print("CCCL ref: summary_statistics.cu (Welford parallel algorithm)") + print("=" * 60) + + results = [] + results.append(("scale_mem_bound parity", test_scale_mem_bound_cccl_parity())) + results.append(("register pressure", test_reduce_register_pressure())) + results.append(("summary_statistics AccumT", test_summary_statistics_accum())) + results.append(("vec alignment", test_vec_alignment())) + test_cccl_sm100_comparison() + + print("\n" + "=" * 60) + all_pass = all(r for _, r in results) + for name, ok in results: + print(f" {'PASS' if ok else 'FAIL'}: {name}") + print(f"\nOverall: {'ALL PASS' if all_pass else 'SOME FAILURES'}") + + return 0 if all_pass else 1 + + +if __name__ == "__main__": + sys.exit(main())