Files
Strata/tools/test_check_cuda_spills.py
Avicennasis 3a3f35c64a tests: cuda_spill_guard fails the CUDA test build when a Strata kernel spills or keeps a per-thread array in local memory (#2683, #2682)
tools/check_cuda_spills.py reads the resource usage the compiler wrote into the kernel libraries (cuobjdump
--dump-resource-usage: STACK and LOCAL per entry function and per sm_) and fails on any Strata kernel outside its ALLOWED
table, which carries the measured allowance of the kernels that hold such an array on purpose; ggml's vendored kernels
(no namespace) are skipped and counted. ctest cuda_spill_guard runs it on eight libraries, skips (77) without cuobjdump or
a demangler, CUDA builds only (HIP has no cuobjdump); check_cuda_spills_unit runs tools/test_check_cuda_spills.py
(stand-in demanglers are sh scripts, so not on Windows).

Changes against the PR: (1) a demangler that exits 0 but prints a blank name used to make the kernel's key empty, which
reads as vendored, so a spilling Strata kernel was skipped; that is now "no verdict" (77) with a test. (2) The unit test is
registered. (3) The ALLOWED table was re-measured on f-027-int: sm_120 with CUDA 13.0 on Windows (MSVC) and sm_86 with the
CUDA 13.0 objects built on Linux (cuobjdump 12.9); allowances are only raised (new DFlash2 gemm8_kernel and
rope_table_kernel, native_gu_swiglu_kernel on sm_86, dequant_kernel / expdec_gu_kernel / prompt_attn_i8v3_kernel /
native_gu_swiglu_kernel on sm_120), none lowered or dropped. Result: 0 violations on both architectures; a test shows the
same object fails once one allowance is lowered.

Ported from PR #2683 (the tracking issue is #2682).
2026-10-26 18:47:35 +01:00

146 lines
6.7 KiB
Python

"""Tests for tools/check_cuda_spills.py, without a CUDA toolkit: synthetic resource rows and stand-in demanglers.
python -m unittest tools.test_check_cuda_spills
"""
from __future__ import annotations
import contextlib
import io
import stat
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))
import check_cuda_spills as CS # noqa: E402
MANGLED = "_ZN6strata7kernels10test_spillEv"
DEMANGLED = "void strata::kernels::test_spill()"
ROWS = [("test.cu.o", "sm_86", MANGLED, {"REG": 32, "STACK": 64, "LOCAL": 0})] # a spilling kernel, not in ALLOWED
def demangler(d, body):
"""An executable stand-in for cu++filt / c++filt: reads names on stdin, runs `body`."""
p = Path(d, "filt")
p.write_text("#!/bin/sh\n" + body + "\n")
p.chmod(p.stat().st_mode | stat.S_IXUSR)
return str(p)
def quiet(fn, *args):
with contextlib.redirect_stdout(io.StringIO()) as out:
rc = fn(*args)
return rc, out.getvalue()
def run_main(d, filt, lib_rows=ROWS, argv=()):
"""main() with tool discovery and cuobjdump's rows stood in: the demangler is the only real process."""
lib = Path(d, "libstrata_kernels.a")
lib.write_bytes(b"")
with mock.patch.object(CS, "find_tools", return_value=("cuobjdump", filt)), \
mock.patch.object(CS, "resource_usage", return_value=lib_rows), \
mock.patch.object(sys, "argv", ["check_cuda_spills.py", *argv, str(lib)]):
return quiet(CS.main)
class Classification(unittest.TestCase):
def test_a_spilling_strata_kernel_is_a_violation(self):
rc, out = quiet(CS.check, "x.a", ROWS, {MANGLED: DEMANGLED})
self.assertEqual(rc, 1)
self.assertIn("not in ALLOWED", out)
def test_the_shipped_table_allows_its_own_measurements_and_a_lowered_allowance_fails(self):
mangled = "_ZN6strata7kernels12_GLOBAL__N_113rope_table_kernelEPKiifPf"
demangled = "strata::kernels::(anonymous namespace)::rope_table_kernel(int const*, int, float, float*)"
rows = [("dflash2.cu.o", "sm_86", mangled, {"REG": 16, "STACK": 32, "LOCAL": 0})]
rc, out = quiet(CS.check, "x.a", rows, {mangled: demangled})
self.assertEqual(rc, 0, out) # the measured frame is the allowance
key = "strata::kernels::{anonymous}::rope_table_kernel"
self.assertEqual(CS.ALLOWED[key]["sm_86"], (32, 0))
with mock.patch.dict(CS.ALLOWED, {key: {"sm_86": (16, 0)}}):
rc, out = quiet(CS.check, "x.a", rows, {mangled: demangled})
self.assertEqual(rc, 1) # one allowance lowered: the same object now fails
self.assertIn("over its allowance STACK 16 LOCAL 0", out)
def test_a_vendored_ggml_kernel_is_skipped_and_counted(self):
rows = [("mmq.cu.o", "sm_86", "_Z9mul_mat_qILi2ELi8EEvPKvS1_PfRiiiiii", {"REG": 128, "STACK": 64})]
rc, out = quiet(CS.check, "x.a", rows, {rows[0][2]: "void mul_mat_q<2, 8>(void const*, void const*, float*, int&, int, int, int, int, int)"})
self.assertEqual(rc, 0)
self.assertIn("1 vendored ggml entries skipped", out)
class Demangling(unittest.TestCase):
"""A mangled name has no "::", so a kernel the demangler did not translate would read as vendored and be skipped:
every way the demangler can be absent or wrong must end in 77, never in a pass."""
def test_no_demangler_is_not_a_pass(self):
rc, out = quiet(CS.demangle, None, [MANGLED])
self.assertIsNone(rc)
with tempfile.TemporaryDirectory() as d:
lib = Path(d, "libstrata_kernels.a")
lib.write_bytes(b"")
with mock.patch.object(CS, "find_tools", return_value=("cuobjdump", None)), \
mock.patch.object(CS, "resource_usage", side_effect=AssertionError("read a library with no demangler")), \
mock.patch.object(sys, "argv", ["check_cuda_spills.py", str(lib)]):
rc, out = quiet(CS.main)
self.assertEqual(rc, 77)
self.assertIn("no demangler", out)
def test_a_demangler_that_exits_nonzero_is_not_a_pass(self):
with tempfile.TemporaryDirectory() as d:
filt = demangler(d, "cat >/dev/null; echo 'no such tool' >&2; exit 3")
self.assertIsNone(quiet(CS.demangle, filt, [MANGLED])[0])
rc, out = run_main(d, filt)
self.assertEqual(rc, 77)
self.assertIn("exited 3", out)
self.assertNotIn("not allowed", out)
def test_a_demangler_that_prints_the_wrong_number_of_lines_is_not_a_pass(self):
with tempfile.TemporaryDirectory() as d:
filt = demangler(d, "cat >/dev/null; exit 0")
self.assertIsNone(quiet(CS.demangle, filt, [MANGLED])[0])
rc, out = run_main(d, filt)
self.assertEqual(rc, 77)
self.assertIn("printed 0 lines for 1 names", out)
def test_a_demangler_that_hands_a_mangled_name_back_is_not_a_pass(self):
with tempfile.TemporaryDirectory() as d:
filt = demangler(d, "cat") # echoes every name as it came
self.assertIsNone(quiet(CS.demangle, filt, [MANGLED])[0])
rc, out = run_main(d, filt)
rc_list, _ = run_main(d, filt, argv=["--list"])
self.assertEqual(rc, 77)
self.assertEqual(rc_list, 77)
self.assertIn("left 1 of 1 names mangled", out)
def test_a_demangler_that_prints_a_blank_name_is_not_a_pass(self):
# exit 0 and one line per name, but the line is empty: the key would be "" and vendored("") is true, so a
# spilling Strata kernel would be skipped as ggml's
with tempfile.TemporaryDirectory() as d:
filt = demangler(d, "while read -r n; do echo; done")
self.assertIsNone(quiet(CS.demangle, filt, [MANGLED])[0])
rc, out = run_main(d, filt)
self.assertEqual(rc, 77)
self.assertIn("printed a blank name for 1 of 1 names", out)
self.assertNotIn("VIOLATION", out)
def test_an_unmangled_name_may_come_back_unchanged(self):
with tempfile.TemporaryDirectory() as d:
filt = demangler(d, "cat")
names, _ = quiet(CS.demangle, filt, ["plain_c_kernel"]) # extern "C": nothing to demangle
self.assertEqual(names, {"plain_c_kernel": "plain_c_kernel"})
def test_a_working_demangler_reaches_the_verdict(self):
with tempfile.TemporaryDirectory() as d:
filt = demangler(d, "while read -r n; do echo '%s'; done" % DEMANGLED)
rc, out = run_main(d, filt)
self.assertEqual(rc, 1)
self.assertIn("VIOLATION %s sm_86: STACK 64 LOCAL 0 (not in ALLOWED)" % DEMANGLED, out)
if __name__ == "__main__":
unittest.main()