#!/usr/bin/env python3 """Instrument a PTX kernel to report where non-finite values first appear. Adds a trailing `.param .u64 d4r_dbg` to the entry and, after every floating-point result written to a %-register (MMA outputs, f16/f16x2 arithmetic, conversions, rsqrt/rcp, ...), ORs a flag into d4r_dbg[source_line] when the value is Inf or NaN (all-ones exponent): bit 0 for an f16/f16x2 result, bit 1 for an f32 result. With --max, each slot instead records the largest magnitude seen on that line (f16 results as f16 bits, f32 results as f32 bits with bit 31 set to tell them apart), which shows where values grow out of range. Replay the instrumented kernel with replay_dlss_layer, passing a zero-filled buffer of 4 * (line_count + 1) bytes as the extra argument, then list the flagged lines with --report. usage: instrument_ptx_nonfinite.py [--max] IN.ptx OUT.ptx KERNEL instrument_ptx_nonfinite.py --report [--max] IN.ptx DEBUG_BUFFER """ import re import struct import sys # Instructions whose first operand (or brace list) is a floating-point result. F16X2_OPS = re.compile(r"^(add|sub|mul|fma\.rn|min|max|neg|abs)\.f16x2$|^cvt\.rn\.f16x2\.e[45]m[23]x2$") F16_OPS = re.compile(r"^(add|sub|mul|fma\.rn|min|max|neg|abs)\.f16$|^cvt\.rn\.f16\.f(32|64)$") F32_OPS = re.compile(r"^(add|sub|mul|fma\.rn|div\.approx|rsqrt\.approx|rcp\.approx|sqrt\.approx|lg2\.approx|ex2\.approx|cvt\.f32\.f16)(\.ftz)?(\.f32)?$") MMA_F16 = re.compile(r"^mma\.sync\.aligned\.m16n8k(16|32)\.row\.col\.f16\.") INSTRUCTION = re.compile(r"^(\s*\{?\s*)(@!?%p\d+\s+)?([a-z][\w.:]*)\s+(.*)$") MAX_MODE = False def max_checks(offset, register, kind): """Record the largest magnitude written by one instruction.""" if kind == "f16x2": return [ f"and.b32 %d4rdbg_r0, {register}, 32767;", f"shr.u32 %d4rdbg_r1, {register}, 16;", "and.b32 %d4rdbg_r1, %d4rdbg_r1, 32767;", "max.u32 %d4rdbg_r0, %d4rdbg_r0, %d4rdbg_r1;", f"atom.global.max.u32 %d4rdbg_r1, [%d4rdbg_rd0+{offset}], %d4rdbg_r0;", ] if kind == "f16": return [ f"cvt.u32.u16 %d4rdbg_r0, {register};", "and.b32 %d4rdbg_r0, %d4rdbg_r0, 32767;", f"atom.global.max.u32 %d4rdbg_r1, [%d4rdbg_rd0+{offset}], %d4rdbg_r0;", ] return [ f"mov.b32 %d4rdbg_r0, {register};", "and.b32 %d4rdbg_r0, %d4rdbg_r0, 2147483647;", "shr.u32 %d4rdbg_r0, %d4rdbg_r0, 1;", "or.b32 %d4rdbg_r0, %d4rdbg_r0, 2147483648;", f"atom.global.max.u32 %d4rdbg_r1, [%d4rdbg_rd0+{offset}], %d4rdbg_r0;", ] def checks_for(line_number, opcode, operands): """Return PTX lines checking the destination of one instruction.""" offset = 4 * line_number if MMA_F16.match(opcode): destination = re.match(r"\{([^}]*)\}", operands) registers = [r.strip() for r in destination.group(1).split(",")] if destination else [] kind = "f16x2" else: first = operands.split(",")[0].strip().rstrip(";") registers = [first] if F16X2_OPS.match(opcode): kind = "f16x2" elif F16_OPS.match(opcode): kind = "f16" elif F32_OPS.match(opcode) and opcode.endswith(("f32", "f16")): kind = "f32" else: return [] lines = [] for register in registers: if not register.startswith("%"): continue # registers declared in nested inline-asm scopes if MAX_MODE: lines += max_checks(offset, register, kind) continue if kind == "f16x2": lines += [ f"and.b32 %d4rdbg_r0, {register}, 2080406528;", # 0x7c007c00 "and.b32 %d4rdbg_r1, %d4rdbg_r0, 31744;", # 0x7c00 "setp.eq.u32 %d4rdbg_p0, %d4rdbg_r1, 31744;", "and.b32 %d4rdbg_r1, %d4rdbg_r0, 2080374784;", # 0x7c000000 "setp.eq.or.u32 %d4rdbg_p0, %d4rdbg_r1, 2080374784, %d4rdbg_p0;", f"@%d4rdbg_p0 atom.global.or.b32 %d4rdbg_r1, [%d4rdbg_rd0+{offset}], 1;", ] elif kind == "f16": lines += [ f"cvt.u32.u16 %d4rdbg_r0, {register};", "and.b32 %d4rdbg_r0, %d4rdbg_r0, 31744;", "setp.eq.u32 %d4rdbg_p0, %d4rdbg_r0, 31744;", f"@%d4rdbg_p0 atom.global.or.b32 %d4rdbg_r1, [%d4rdbg_rd0+{offset}], 1;", ] else: lines += [ f"mov.b32 %d4rdbg_r0, {register};", "and.b32 %d4rdbg_r0, %d4rdbg_r0, 2139095040;", # 0x7f800000 "setp.eq.u32 %d4rdbg_p0, %d4rdbg_r0, 2139095040;", f"@%d4rdbg_p0 atom.global.or.b32 %d4rdbg_r1, [%d4rdbg_rd0+{offset}], 2;", ] return lines def instrument(source, kernel): lines = source.split("\n") output = [] in_kernel = False entry_params_open = False body_started = False pending_statement = None # multi-line mma statements for number, line in enumerate(lines, start=1): if re.match(rf"^\.visible \.entry {re.escape(kernel)}\(", line): in_kernel = True entry_params_open = True output.append(line) continue if in_kernel and entry_params_open and line.strip() == ")": output[-1] = output[-1] + "," output.append(".param .u64 d4r_dbg") output.append(line) entry_params_open = False continue if in_kernel and not body_started and line.strip() == "{": output.append(line) output.append(".reg .pred %d4rdbg_p0;") output.append(".reg .b32 %d4rdbg_r<2>;") output.append(".reg .b64 %d4rdbg_rd0;") output.append("ld.param.u64 %d4rdbg_rd0, [d4r_dbg];") output.append("cvta.to.global.u64 %d4rdbg_rd0, %d4rdbg_rd0;") body_started = True continue output.append(line) if not body_started: continue if line.strip() == "}" and pending_statement is None and number == len(lines): in_kernel = False statement = line if pending_statement is None else pending_statement[1] + " " + line start = number if pending_statement is None else pending_statement[0] if ";" not in line: match = INSTRUCTION.match(statement) if match and MMA_F16.match(match.group(3)): pending_statement = (start, statement) continue pending_statement = None match = INSTRUCTION.match(statement) if not match or match.group(2): continue # skip predicated results: the check would run unconditionally checks = checks_for(start, match.group(3), match.group(4)) if checks: output.extend(checks) return "\n".join(output), len(lines) def half_value(bits): return struct.unpack("