Skip to content

Commit f4e2d65

Browse files
committed
Add --verbose flag to verify_codegen.py
1 parent b6bb28e commit f4e2d65

3 files changed

Lines changed: 111 additions & 46 deletions

File tree

.github/workflows/cmake-run-cuda.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ jobs:
3434
with:
3535
submodules: 'true'
3636

37+
# Run codegen tests first, so things fail immediately before building the full project
38+
- name: Codegen tests
39+
# These check exact instruction counts in the generated PTX, which can legitimately
40+
# differ between nvcc versions even when the code itself is correct.
41+
run: python3 tests_codegen/verify_codegen.py tests_codegen/*.cu --nvcc nvcc
42+
3743
- name: Configure CMake
3844
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
3945
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
@@ -48,8 +54,3 @@ jobs:
4854
# Execute tests defined by the CMake configuration.
4955
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
5056
run: ./tests/kernel_float_tests --durations=yes --success --verbosity=high ~[GPU]
51-
52-
- name: Codegen instruction checks
53-
# These check exact instruction counts in the generated PTX, which can legitimately
54-
# differ between nvcc versions even when the code itself is correct.
55-
run: python3 tests_codegen/verify_codegen.py tests_codegen/*.cu --nvcc nvcc

tests_codegen/bf16_ops.cu

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,8 @@ __global__ void bfloat16_eq(const kf::bfloat16x4 *input, bool *output) {
8181
output[0] = kf::all(*input == *input);
8282
}
8383

84-
// Division is not packed at all: each of the 4 lanes is extracted individually and divided
85-
// with a scalar `div.rn.f32`, then converted back with `cvt.rn.bf16.f32`.
86-
// CHECK-COUNT-4: div.rn.f32
84+
// Division is not packed at all: each of the 4 lanes is extracted individually.
85+
// CHECK-COUNT-4: div.(rn|approx).f32
8786
// CHECK-NOT: div.bf16x2
8887
__global__ void bfloat16_div(const kf::bfloat16x4 *input, kf::bfloat16x4 *output) {
8988
*output = *input / *input;

tests_codegen/verify_codegen.py

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -54,38 +54,33 @@
5454

5555

5656
class Kernel:
57-
def __init__(self, name):
57+
def __init__(self, name, requires):
5858
self.name = name
59-
self.requires = [] # list of (pattern, expected_count)
60-
self.forbids = []
59+
self.requires = requires # list of (pattern, expected_count)
6160

6261

6362
def parse_kernels(source: Path):
6463
kernels = []
65-
pending_requires, pending_forbids = [], []
64+
pending_requires = []
6665
arch = None
6766

6867
for line in source.read_text().splitlines():
6968
if m := CHECK_COUNT_RE.search(line):
7069
pending_requires.append((m.group(2).strip(), int(m.group(1))))
7170
elif m := CHECK_RE.search(line):
72-
# `None` means "at least once" (exact count is not checked)
7371
pending_requires.append((m.group(1).strip(), 1))
7472
elif m := CHECK_NOT_RE.search(line):
75-
pending_forbids.append(m.group(1).strip())
73+
pending_requires.append((m.group(1).strip(), 0))
7674
elif m := ARCH_RE.search(line):
7775
arch = m.group(1)
7876
elif m := KERNEL_RE.search(line):
79-
kernel = Kernel(m.group(1))
80-
kernel.requires = pending_requires
81-
kernel.forbids = pending_forbids
82-
kernels.append(kernel)
83-
pending_requires, pending_forbids = [], []
77+
kernels.append(Kernel(m.group(1), pending_requires))
78+
pending_requires = []
8479

8580
return kernels, arch
8681

8782

88-
def get_ptx(nvcc, source, arch):
83+
def get_ptx(nvcc, source, arch, verbose=False):
8984
kf_root = Path(__file__).parent.parent.resolve() / "include"
9085
cmd = [
9186
nvcc,
@@ -99,6 +94,9 @@ def get_ptx(nvcc, source, arch):
9994
str(source),
10095
]
10196

97+
if verbose:
98+
print(f"$ {' '.join(cmd)}", file=sys.stderr)
99+
102100
result = subprocess.run(cmd, capture_output=True, text=True)
103101
if result.returncode != 0:
104102
print(f"command failed: {' '.join(cmd)}", file=sys.stderr)
@@ -128,25 +126,49 @@ def extract_ptx_block(ptx: str, kernel_name: str) -> str:
128126
return None
129127

130128

131-
def check_file(source: Path, nvcc: str, arch: str) -> "tuple[int, int]":
132-
"""Returns (num_kernels, num_failed) for this source file."""
133-
kernels, arch_directive = parse_kernels(source)
129+
def check_file(
130+
source: Path,
131+
nvcc: str,
132+
arch: str,
133+
verbose: bool = False,
134+
kernel_filter: "set[str] | None" = None,
135+
) -> "tuple[int, int, int]":
136+
"""Returns (num_passed, num_failed, num_skipped) for this source file."""
137+
all_kernels, arch_directive = parse_kernels(source)
134138
arch = arch_directive or arch
135139

136-
kernels = [k for k in kernels if k.requires or k.forbids]
137-
if not kernels:
140+
checkable = [k for k in all_kernels if k.requires]
141+
if not checkable:
138142
print(
139143
f"error: {source} has no kernel preceded by "
140144
"// CHECK:, // CHECK-COUNT-<N>:, or // CHECK-NOT: directives",
141145
file=sys.stderr,
142146
)
143-
return 0, 1
147+
return 0, 1, 0
148+
149+
# Kernels with no directives at all are skipped silently; they were never meant to be
150+
# checked. Kernels excluded by --kernel are also skipped, but reported below.
151+
num_skipped = len(all_kernels) - len(checkable)
152+
153+
if kernel_filter is not None:
154+
kernels = [k for k in checkable if k.name in kernel_filter]
155+
num_skipped += len(checkable) - len(kernels)
156+
for k in checkable:
157+
if k.name not in kernel_filter:
158+
print(f"SKIP: {k.name} (excluded by --kernel)")
159+
else:
160+
kernels = checkable
161+
162+
if not kernels:
163+
# Nothing in this file matches --kernel; not an error, just nothing to do here.
164+
return 0, 0, num_skipped
144165

145-
ptx = get_ptx(nvcc, source, arch)
166+
ptx = get_ptx(nvcc, source, arch, verbose=verbose)
146167
if ptx is None:
147168
print(f"FAIL: {source}: nvcc failed to compile this file", file=sys.stderr)
148-
return len(kernels), len(kernels)
169+
return 0, len(kernels), num_skipped
149170

171+
num_passed = 0
150172
num_failed = 0
151173

152174
for kernel in kernels:
@@ -162,18 +184,24 @@ def check_file(source: Path, nvcc: str, arch: str) -> "tuple[int, int]":
162184

163185
failures = []
164186
for pattern, expected_count in kernel.requires:
165-
if expected_count is None:
166-
if not re.search(pattern, block, re.MULTILINE):
167-
failures.append(f"CHECK pattern not found: {pattern!r}")
168-
else:
169-
actual_count = len(re.findall(pattern, block, re.MULTILINE))
170-
if actual_count != expected_count:
171-
failures.append(
172-
f"CHECK pattern found {actual_count} time(s), expected {expected_count}: {pattern!r}"
173-
)
174-
for pattern in kernel.forbids:
175-
if re.search(pattern, block, re.MULTILINE):
176-
failures.append(f"CHECK-NOT pattern found: {pattern!r}")
187+
actual_count = len(re.findall(pattern, block, re.MULTILINE))
188+
ok = (
189+
actual_count >= 1
190+
if expected_count is None
191+
else actual_count == expected_count
192+
)
193+
want = "at least 1" if expected_count is None else str(expected_count)
194+
195+
if verbose:
196+
status = "success" if ok else "FAILURE"
197+
print(
198+
f"[{kernel.name}] {status} (want {want}, found {actual_count}): {pattern!r}",
199+
file=sys.stderr,
200+
)
201+
if not ok:
202+
failures.append(
203+
f"pattern found {actual_count} time(s), expected {want}: {pattern!r}"
204+
)
177205

178206
if failures:
179207
num_failed += 1
@@ -184,9 +212,16 @@ def check_file(source: Path, nvcc: str, arch: str) -> "tuple[int, int]":
184212
)
185213
print(block, file=sys.stderr)
186214
else:
215+
num_passed += 1
187216
print(f"OK: {kernel.name} (ptx, {arch})")
217+
if verbose:
218+
print(
219+
f"----- generated ptx for {kernel.name} ({arch}) -----",
220+
file=sys.stderr,
221+
)
222+
print(block, file=sys.stderr)
188223

189-
return len(kernels), num_failed
224+
return num_passed, num_failed, num_skipped
190225

191226

192227
def main():
@@ -195,22 +230,52 @@ def main():
195230
)
196231
parser.add_argument("source", type=Path, nargs="+")
197232
parser.add_argument("--nvcc", default="nvcc")
198-
parser.add_argument("--verbose", action="store_true")
233+
parser.add_argument(
234+
"--verbose",
235+
action="store_true",
236+
help="print the nvcc command, per-pattern match results, and the generated PTX "
237+
"for every kernel, not just the ones that fail",
238+
)
199239
parser.add_argument(
200240
"--arch", help="used unless overridden by a // ARCH: directive", default="sm_80"
201241
)
242+
parser.add_argument(
243+
"--kernel",
244+
action="append",
245+
help="only check the kernel with this name, skipping all others (repeatable)",
246+
)
202247
args = parser.parse_args()
203248

204-
total_kernels = 0
249+
kernel_filter = set(args.kernel) if args.kernel else None
250+
251+
total_passed = 0
205252
total_failed = 0
253+
total_skipped = 0
206254

207255
for source in args.source:
208256
print(f"=== {source} ===")
209-
num_kernels, num_failed = check_file(source, args.nvcc, args.arch)
210-
total_kernels += num_kernels
257+
num_passed, num_failed, num_skipped = check_file(
258+
source,
259+
args.nvcc,
260+
args.arch,
261+
verbose=args.verbose,
262+
kernel_filter=kernel_filter,
263+
)
264+
total_passed += num_passed
211265
total_failed += num_failed
266+
total_skipped += num_skipped
212267

213-
print(f"TEST PASSED: {total_kernels - total_failed} / {total_kernels}")
268+
if kernel_filter is not None and total_passed + total_failed == 0:
269+
print(
270+
f"error: no kernel named {sorted(kernel_filter)} found in any source file",
271+
file=sys.stderr,
272+
)
273+
sys.exit(1)
274+
275+
total = total_passed + total_failed + total_skipped
276+
print(
277+
f"RESULT: {total_passed} passed, {total_failed} failed, {total_skipped} skipped ({total} total)"
278+
)
214279

215280
if total_failed:
216281
sys.exit(1)

0 commit comments

Comments
 (0)