Files
d4r/kernels/tools/compare_launch_dump.py
woutermans 708b158332 kernels: compare two per-launch dumps byte for byte
The bridge's other instrumentation - D4R_CUDA_LAUNCH_STATS with
D4R_CUDA_LAUNCH_DUMP_DIR - already dumps every allocation, surface object and
texture object named in a launch's packed argument buffer, after the launch has
finished. That is enough to check one reimplemented kernel on its own: the
translated kernel's surface files are its output, and a native replacement has
to reproduce them.

What was missing was the comparison. compare_launch_dump.py keys the files by
kernel, argument offset, kind, shape and format rather than by the global launch
sequence, so a replacement that adds launches of its own does not break the
alignment, and it reports the first differing byte per argument.

Verified end to end today: cuda_dldn_engine_swin_enc0_kernel's PTX module was
compiled offline with d4r_emit, served from a native directory (the bridge logs
`[d4r-native] "cuda_dldn_engine_swin_enc0_kernel" -> ...hsaco`), and its run is
byte-identical to the translated one across all 54 of its dumped arguments -
weight image, activation buffer, four output surfaces. That also shows the
offline build reproduces the loader's own compile exactly, which every native
denoiser layer will rest on.

docs/native-kernels.md gains the dump-and-diff step, which is the only way to
localise a mismatch to one of the eleven layers that sample textures and write
surfaces.
2026-10-12 12:47:53 +02:00

101 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Compare two D4R_CUDA_LAUNCH_DUMP_DIR trees byte for byte.
usage: compare_launch_dump.py A_DIR B_DIR [--kernel SUBSTR] [--quiet]
The bridge writes one file per launch and per argument named
`launch-NNN-<kernel>-argNNN-<kind>-<shape>-fmtN.bin` (wine_nvcuda_bridge.c
summarize_resource). The launch number is a global sequence, so it is not a
stable key: a native kernel that runs its own prep shifts every later number.
Files are therefore keyed by (kernel, argument offset, kind, shape, format) and
by how many times that key has occurred so far, which survives a shift as long
as each kernel is launched the same number of times.
Every dump is written after the launch returns, so a surface file is that
layer's own output and a read-only argument (a weight image, a texture) is its
input. Any difference in an output is the kernel's; identical read-only
arguments are the check that the two runs are aligned at all.
"""
import argparse
import os
import re
import sys
NAME = re.compile(r'^launch-(\d+)-(.+)-arg(\d+)-(buffer|surface|texture)-(.+)-fmt(-?\d+)\.bin$')
# A buffer's shape string embeds the device pointer it was found at, which differs
# between runs. Keep the offset within the allocation, drop the base address.
BASE = re.compile(r'base0x[0-9a-f]+')
def index(directory):
"""(kernel, arg, kind, shape, fmt, occurrence) -> (sequence, path)."""
entries = {}
counts = {}
for name in sorted(os.listdir(directory)):
match = NAME.match(name)
if match is None:
continue
sequence, kernel, argument, kind, shape, fmt = match.groups()
key = (kernel, int(argument), kind, BASE.sub('base', shape), int(fmt))
occurrence = counts.get(key, 0)
counts[key] = occurrence + 1
entries[key + (occurrence,)] = (int(sequence), os.path.join(directory, name))
return entries
def first_difference(a, b):
"""(offset, count) of the differing bytes, or None when equal."""
if len(a) != len(b):
return None
count = 0
first = -1
for index_ in range(len(a)):
if a[index_] != b[index_]:
if first < 0:
first = index_
count += 1
return (first, count) if count else None
def main(argv):
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('a')
parser.add_argument('b')
parser.add_argument('--kernel', default='', help='only keys whose kernel name contains this')
parser.add_argument('--quiet', action='store_true', help='report only keys that differ')
args = parser.parse_args(argv[1:])
left, right = index(args.a), index(args.b)
keys = sorted(set(left) | set(right))
mismatched = missing = compared = 0
for key in keys:
if args.kernel and args.kernel not in key[0]:
continue
label = '%s arg%03d %s %s fmt%d #%d' % key
if key not in left or key not in right:
print('MISSING %s: %s' % (label, 'only in A' if key in left else 'only in B'))
missing += 1
continue
with open(left[key][1], 'rb') as handle:
a = handle.read()
with open(right[key][1], 'rb') as handle:
b = handle.read()
compared += 1
difference = first_difference(a, b)
if difference is None:
if not args.quiet:
print('ok %s (%d bytes)' % (label, len(a)))
else:
offset, count = difference
print('DIFFERS %s: %d of %d bytes, first at %d (A %02x B %02x)' %
(label, count, len(a), offset, a[offset], b[offset]))
mismatched += 1
print('%d arguments compared, %d differ, %d missing' % (compared, mismatched, missing))
return 1 if mismatched or missing else 0
if __name__ == '__main__':
sys.exit(main(sys.argv))