Port K kernel tuning and M gfx1101 flags

Preset K, byte-identical to the previous output in all captured buffers
and the harness image, measured on an RX 7700 XT:

- LDS row padding so WMMA operand loads hit distinct banks

- WGP mode for enc1/enc2/dec1/dec2, ds_bpermute half-wave exchange for
  enc0/dec0/dec3, more waves per window in enc4/dec4/dec5

- cheaper operand plumbing in common/wmma_layout.h (packed f16
  conversions, no dynamic vector indexing), raw buffer weight loads,
  accumulator-init vectors prepared once

- enc1 as a single-buffer recompute layer, dec1 Q/K/V chains split

- dec5 as four dependent phase kernels
This commit is contained in:
ayo committed 2026-10-05 14:48:37 -04:00
1 parent 8ce8618181
commit 22026ede12
45 files changed
+4514 -354

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# build_native.sh SRC.hip OUT.hsaco [extra clang flags]: natively compiled DLSS texture kernel (gfx1101, wave64 default)
set -euo pipefail
ND=$(cd "$(dirname "$0")" && pwd)
SRC=$(realpath "$1") OUT=$(realpath -m "$2"); shift 2
[[ -s "$SRC" ]] || { echo "empty native source: $SRC" >&2; exit 2; }
R=${D4R_ROCM_DIR:-/opt/rocm}
ARCH=${D4R_GPU_ARCH:-gfx1101}
W=${WAVE:-64}
WF=$([[ $W == 64 ]] && echo -mwavefrontsize64 || echo -mno-wavefrontsize64)
D=$(cd "$(dirname "$SRC")" && pwd)
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
(cd "$T" && "$R/lib/llvm/bin/clang++" -x hip -std=c++20 --offload-arch=$ARCH --offload-device-only -O3 $WF -mcumode \
-ffp-contract=off -fgpu-flush-denormals-to-zero -fno-fast-math -I"$D" -I"$ND" -I"$R/include" --rocm-path="$R" \
--rocm-device-lib-path="$R/amdgcn/bitcode" "$@" -o "$OUT" "$SRC" -save-temps=cwd --no-gpu-bundle-output)
S=$(ls "$T"/*.s | head -1)
cp "$S" "${OUT%.hsaco}.s"
printf '%s: %s\n' "$(basename "$OUT")" "$(grep -hE '^\s+\.(vgpr_count|sgpr_count|private_segment_fixed_size|vgpr_spill_count|group_segment_fixed_size):' "$S" | tr -s ' ' | paste -sd' ')"
+411
View File
@@ -0,0 +1,411 @@
"""Color-tap LDS tile pass for the hiluma output kernels (used by ptx2hip.py --ctile).
Path A of hiluma_engine_output_* (taken when a block's render footprint is wider than path B's 24-texel LDS tile,
e.g. at Quality) fetches 16 color texels per thread and converts each one (YCoCg, exposure, tonemap weight) right
after the fetch. The conversion of a texel depends only on the texel and the exposure %f1, and neighbouring
threads convert the same texels (~9x at ratio 1.5). This pass:
1. finds every tex.base on the color texture inside path A,
2. slices the instructions that compute the tap's packed result (cvt.rn.f16x2 x2 or cvt.rn.f16.f32) from the
fetched texel (forward taint + backward need, including the tonemap branch diamond), and checks that no other
value escapes the slice,
3. replaces the slice with `d4r.ctap.G` (tile lookup, falling back to fetch + the same conversion), and
4. emits one conversion function per slice shape (generated from the slice's own instructions, so the tile holds
bit-identical values) and a cooperative tile fill at the start of path A.
The tile is keyed by the final (clamped) texel coordinates, so any tap outside it just takes the fallback.
"""
import re
REG = re.compile(r'%[A-Za-z_$][\w$]*')
CAP = 1024 # tile texels (TW*TH); a larger footprint falls back to direct fetches
class CtileErr(Exception):
pass
def parse(body, tokenize):
"""-> list of dicts: kind ('stmt'|'label'|'open'|'close'), text, plus op/dst/src/pred for stmts"""
items = []
for kind, text in tokenize(body):
it = {'kind': kind, 'text': text}
if kind == 'stmt':
s = text
m = re.match(r'@(!?)(%p\d+)\s+(.*)$', s)
it['pred'] = m.group(2) if m else None
if m:
s = m.group(3)
parts = s.split(None, 1)
it['op'] = parts[0]
rest = parts[1] if len(parts) > 1 else ''
it['rest'] = rest
dst, src = [], []
op = it['op']
if op.startswith('.') or op.startswith('bra') or op.startswith('bar') or op.startswith('ret'):
src = REG.findall(rest)
elif op.startswith('st.') or op.startswith('sust'):
src = REG.findall(rest)
else:
# first operand = destination(s)
depth, i = 0, 0
while i < len(rest):
c = rest[i]
if c in '{[':
depth += 1
elif c in '}]':
depth -= 1
elif c == ',' and depth == 0:
break
i += 1
dst = REG.findall(rest[:i])
src = REG.findall(rest[i:])
if it['pred']:
src = src + [it['pred']]
it['dst'], it['src'] = dst, src
items.append(it)
return items
def branch_target(it):
return it['rest'].strip().rstrip(';').strip().lstrip('$') if it['kind'] == 'stmt' and it['op'].startswith('bra') else None
def find_taps(items, color_aliases):
return [i for i, it in enumerate(items) if it['kind'] == 'stmt' and it['op'] == 'tex.base.2d.v4.f32.s32'
and re.search(r'\[\s*(%rd\d+)', it['rest']).group(1) in color_aliases]
def slice_tap(items, t):
"""-> (removed index set, results list, kind 'in'|'out', externals set)"""
tex = items[t]
taint = set(tex['dst'])
tainted_idx = [t]
results, kind = [], None
i = t + 1
while i < len(items):
it = items[i]
if it['kind'] == 'stmt' and any(s in taint for s in it['src']):
op = it['op']
if op.startswith('cvt.rn.f16x2.f32') and kind in (None, 'in'):
kind = 'in'
results.append((i, it['dst'][0]))
tainted_idx.append(i)
if len(results) == 2:
break
elif op.startswith('cvt.rn.f16.f32') and kind is None:
kind = 'out'
results.append((i, it['dst'][0]))
tainted_idx.append(i)
break
else:
taint.update(it['dst'])
tainted_idx.append(i)
i += 1
else:
raise CtileErr(f'tap at {t}: no packed result found')
last = results[-1][0]
# backward need from the results (within [t, last])
need_regs = set()
for ri, _ in results:
need_regs.update(items[ri]['src'])
needed = {ri for ri, _ in results}
# conditional branches on tainted predicates (the tonemap branch) are part of the slice
for j in tainted_idx:
it = items[j]
if it['op'].startswith('bra') and it['pred'] is not None:
needed.add(j)
need_regs.add(it['pred'])
for j in range(last, t - 1, -1):
it = items[j]
if j in needed:
continue
if it['kind'] == 'stmt' and j in tainted_idx and any(d in need_regs for d in it['dst']):
needed.add(j)
need_regs.update(it['src'])
extra = [j for j in tainted_idx if j not in needed]
if extra:
raise CtileErr(f'tap at {t}: tainted but not needed: {[items[j]["text"] for j in extra]}')
removed = set(needed)
# branch diamonds: region from each conditional branch to its merge label
for j in sorted(needed):
it = items[j]
if not it['op'].startswith('bra'):
continue
targets, seen = {branch_target(it)}, set()
k = j + 1
while True:
x = items[k]
if x['kind'] == 'label':
lab = x['text'].lstrip('$')
if lab in targets and targets - seen == {lab} and lab not in {branch_target(it)}:
removed.add(k)
break
seen.add(lab)
removed.add(k)
elif x['kind'] == 'stmt':
if x['op'].startswith('bra'):
if x['pred'] is not None and k not in needed:
raise CtileErr(f'tap at {t}: foreign conditional branch in diamond')
targets.add(branch_target(x))
removed.add(k)
elif k not in needed:
# constant moves used only inside the slice are part of it
if x['op'].startswith('mov') and not any(REG.match(s) for s in x['src']):
removed.add(k)
else:
raise CtileErr(f'tap at {t}: foreign instruction in diamond: {x["text"]}')
k += 1
if k > last:
raise CtileErr(f'tap at {t}: diamond does not close before the result')
# escape check: registers defined in the slice (except results) used outside it
defs = set()
for j in removed:
if items[j]['kind'] == 'stmt':
defs.update(items[j]['dst'])
res_regs = {r for _, r in results}
inner_defs = defs - res_regs
for j, it in enumerate(items):
if j in removed or it['kind'] != 'stmt':
continue
bad = inner_defs.intersection(it['src'])
if bad:
raise CtileErr(f'tap at {t}: {bad} escapes to {it["text"]}')
if defs.intersection(it['dst']) - res_regs or res_regs.intersection(it['dst']):
raise CtileErr(f'tap at {t}: slice register redefined by {it["text"]}')
# labels removed must not be targeted from outside
labels = {items[j]['text'].lstrip('$') for j in removed if items[j]['kind'] == 'label'}
for j, it in enumerate(items):
if j not in removed and branch_target(it) in labels:
raise CtileErr(f'tap at {t}: label targeted from outside the slice')
used = set()
for j in removed:
if items[j]['kind'] == 'stmt' and j != t:
used.update(items[j]['src'])
externals = used - defs - set(tex['dst'])
externals.discard(None)
return removed, results, kind, externals
def canon(items, removed, t, results):
"""canonical text of the slice (registers by first appearance, texel by position)"""
names, labels = {}, {}
for k, r in enumerate(items[t]['dst']):
names[r] = f'T{k}'
for k, (_, r) in enumerate(results):
names[r] = f'R{k}'
out = []
for j in sorted(removed):
if j == t:
continue
it = items[j]
if it['kind'] == 'label':
lab = it['text'].lstrip('$')
labels.setdefault(lab, f'L{len(labels)}')
out.append(labels[lab] + ':')
continue
if it['kind'] != 'stmt':
continue
s = it['text']
def rn(m):
r = m.group(0)
if r not in names:
names[r] = f'V{len(names)}'
return names[r]
s = REG.sub(rn, s)
s = re.sub(r'\$(L__\w+)', lambda m: labels.setdefault(m.group(1), f'L{len(labels)}'), s)
out.append(s)
return '\n'.join(out), names
def apply(tr, tokenize):
"""Rewrite tr.body in place for the color tile. Returns the HIP prelude (conversion functions) and info."""
body = tr.body
items = parse(body, tokenize)
# color texture handle: ld.param.u64 [%rd1+-56] (param+200, %rd10 / %rd69); taps use it or its mov aliases
color = {it['dst'][0] for it in items if it['kind'] == 'stmt' and it['op'] == 'ld.param.u64'
and re.search(r'\[%rd1\+-56\]', it['rest'])}
aliases = set(color)
for it in items:
if it['kind'] == 'stmt' and it['op'] == 'mov.u64' and len(it['src']) == 1 and it['src'][0] in color:
aliases.add(it['dst'][0])
# path A's color taps lie between $L__BB0_1 and $L__BB0_77 (the per-tap blocks); path B's are elsewhere
labels = {it['text'].lstrip('$'): i for i, it in enumerate(items) if it['kind'] == 'label'}
a0, a1 = labels['L__BB0_1'], labels['L__BB0_77']
taps = [t for t in find_taps(items, aliases) if a0 < t < a1]
if len(taps) != 16:
raise CtileErr(f'expected 16 path A color taps, found {len(taps)}')
groups, per_tap = {}, []
all_removed = set()
for t in taps:
removed, results, kind, ext = slice_tap(items, t)
if removed & all_removed:
raise CtileErr('overlapping slices')
all_removed |= removed
text, names = canon(items, removed, t, results)
ext_names = sorted(ext, key=lambda r: names.get(r, r))
key = (kind, text, tuple(names.get(r) for r in ext_names))
g = groups.setdefault(key, len(groups))
per_tap.append((t, removed, results, kind, ext_names, g))
if len(groups) > 3:
raise CtileErr(f'{len(groups)} slice shapes')
# externals: %f1 (exposure) or constant registers (single mov-immediate definition)
const_def = {}
for it in items:
if it['kind'] == 'stmt' and it['op'].startswith('mov') and len(it['dst']) == 1 and not it['src']:
const_def.setdefault(it['dst'][0], []).append(it)
group_info = {}
for (t, removed, results, kind, ext_names, g) in per_tap:
if g in group_info:
continue
params, consts = [], []
for r in ext_names:
if r == '%f1':
params.append(r)
elif r in const_def and len(const_def[r]) == 1:
consts.append(const_def[r][0]['text'])
else:
raise CtileErr(f'external {r} of tap {t}')
group_info[g] = dict(t=t, removed=removed, results=results, kind=kind, params=params, consts=consts)
return items, per_tap, group_info, color
def emit(tr, tokenize, Translator):
"""Apply the pass to translator tr (before translate()). Installs the pseudo-op handler; returns prelude text."""
items, per_tap, group_info, color = apply(tr, tokenize)
# ---- conversion functions, one per group, translated from the group's first slice
prelude = []
for g, info in sorted(group_info.items()):
t, removed, results = info['t'], info['removed'], info['results']
sub = Translator.__new__(Translator)
sub.kernel = tr.kernel
sub.param_name = tr.param_name
sub.regs, sub.shared, sub.locals, sub.lines = {}, [], [], []
sub.scope_names, sub.scope_id = [{}], 0
tex = items[t]
# texel inputs
for k, r in enumerate(tex['dst']):
sub.reg(r)
sub.lines.append(f' {sub.reg(r)} = t_[{k}];')
for c in info['consts']:
sub.stmt(c)
for j in sorted(removed):
if j == t:
continue
it = items[j]
if it['kind'] == 'label':
sub.lines.append(f'{it["text"].replace("$", "")}:;')
elif it['kind'] == 'stmt':
sub.stmt(it['text'])
outs = [sub.reg(r) for _, r in results]
decl = []
for name, ty in sorted(sub.regs.items()):
if name in [sub.reg(p) for p in info['params']]:
continue
decl.append(f' {ty} {name};')
args = ', '.join(['f4 t_'] + [f'float {sub.reg(p)}' for p in info['params']] + [f'uint32_t* o{k}' for k in range(len(outs))])
prelude.append(f'RT void d4r_ctap_conv{g}({args})\n{{')
prelude.extend(decl)
prelude.extend(sub.lines)
for k, o in enumerate(outs):
prelude.append(f' *o{k} = (uint32_t){o};')
prelude.append('}')
# ---- tile storage (aliases the kernel's LDS through d4r_lds)
offs, off = {}, 0
for g, info in sorted(group_info.items()):
for k in range(len(info['results'])):
offs[(g, k)] = off
off += CAP * 4
# ---- rewrite the body: removed statements dropped, pseudo-op at each tap, fill at the start of path A
tap_at = {t: (results, ext, g) for (t, removed, results, kind, ext, g) in per_tap}
removed_all = set()
for (t, removed, *_r) in per_tap:
removed_all |= removed
out = []
for i, it in enumerate(items):
if it['kind'] == 'open':
out.append('{'); continue
if it['kind'] == 'close':
out.append('}'); continue
if it['kind'] == 'label':
if i in removed_all:
continue
out.append(it['text'] + ':')
if it['text'].lstrip('$') == 'L__BB0_1':
out.append('d4r.ctile_fill;')
continue
if i in tap_at:
results, ext, g = tap_at[i]
m = re.search(r'\[\s*(%rd\d+)\s*,\s*\{([^}]*)\}\s*\]', it['rest'])
regs = ', '.join(r for _, r in results)
out.append(f'd4r.ctap.{g} {regs}, {m.group(1)}, {m.group(2)};')
continue
if i in removed_all:
continue
out.append(it['text'] + ';')
tr.body = '\n'.join(out)
summary = dict(groups=group_info, offs=offs, lds_bytes=off, taps=len(per_tap))
orig_op = tr.op
def op(opname, a):
if opname == 'd4r.ctile_fill':
tr.emit('d4r_ctile_fill(KARGP, (int32_t)r3, (int32_t)r4, (int32_t)r5, (int32_t)r6, f1, &ct_x0, &ct_y0, &ct_w, &ct_h);')
return
if opname.startswith('d4r.ctap.'):
g = int(opname.split('.')[2])
nres = len(group_info[g]['results'])
res = [tr.reg(x) for x in a[:nres]]
h = tr.val(a[nres], 'u64')
x, y = tr.val(a[nres + 1], 'u32'), tr.val(a[nres + 2], 'u32')
prm = ''.join(f', {tr.val(p, "f32")}' for p in group_info[g]['params'])
tr.emit(f'{{ int32_t dx_ = (int32_t){x} - ct_x0, dy_ = (int32_t){y} - ct_y0;')
tr.emit(f' if ((uint32_t)dx_ < (uint32_t)ct_w && (uint32_t)dy_ < (uint32_t)ct_h) {{ uint32_t i_ = (uint32_t)(dy_ * ct_w + dx_);')
for k, r in enumerate(res):
tr.emit(f' {r} = (uint32_t)((const AS3 uint32_t*)(d4r_lds + {offs[(g, k)]}))[i_];')
outs = ', '.join(f'o_ + {k}' for k in range(nres))
tr.emit(f' }} else {{ uint32_t o_[2]; d4r_ctap_conv{g}(tex_fetch({h}, (int32_t){x}, (int32_t){y}){prm}, {outs});')
for k, r in enumerate(res):
tr.emit(f' {r} = o_[{k}];')
tr.emit(' } }')
return
orig_op(opname, a)
tr.op = op
# result register types: f16x2 packs are u32 regs, f16 packs are u16 regs: both assigned via (uint32_t) casts
# fill function (needs the conversion functions and offsets)
fill = ['RT void d4r_ctile_fill(const AS4 uint8_t* KARGP, int32_t x0l, int32_t y0l, int32_t x1l, int32_t y1l, float f1,',
' int32_t* ox0, int32_t* oy0, int32_t* ow, int32_t* oh)',
'{',
' // final texel coordinate of a tile-relative logical coordinate u: u > 0 ? min(lo + u, hi) : lo',
' // (the clamp every color tap applies; params +208/+212 = lo, +216/+220 = hi)',
' int32_t lox = *(const AS4 int32_t*)(KARGP + 208), loy = *(const AS4 int32_t*)(KARGP + 212);',
' int32_t hix = *(const AS4 int32_t*)(KARGP + 216), hiy = *(const AS4 int32_t*)(KARGP + 220);',
' int32_t fx0 = x0l > 0 ? __builtin_elementwise_min(lox + x0l, hix) : lox;',
' int32_t fy0 = y0l > 0 ? __builtin_elementwise_min(loy + y0l, hiy) : loy;',
' int32_t fx1 = x1l > 0 ? __builtin_elementwise_min(lox + x1l, hix) : lox;',
' int32_t fy1 = y1l > 0 ? __builtin_elementwise_min(loy + y1l, hiy) : loy;',
' int32_t w = fx1 - fx0 + 1, h = fy1 - fy0 + 1;',
f' if (w <= 0 || h <= 0 || w * h > {CAP}) {{ w = 0; h = 0; }}',
' uint64_t tex = *(const AS4 uint64_t*)(KARGP + 200);',
' int32_t n = w * h;',
' for (int32_t i = (int32_t)(__builtin_amdgcn_workitem_id_y() * 16u + __builtin_amdgcn_workitem_id_x()); i < n; i += 256)',
' {',
' int32_t ty = i / w, tx = i - ty * w;',
' f4 t_ = tex_fetch(tex, fx0 + tx, fy0 + ty);']
for g, info in sorted(group_info.items()):
nres = len(info['results'])
prm = ''.join(', f1' for _ in info['params'])
outs = ', '.join(f'o_ + {k}' for k in range(nres))
fill.append(f' {{ uint32_t o_[2]; d4r_ctap_conv{g}(t_{prm}, {outs});')
for k in range(nres):
fill.append(f' ((AS3 uint32_t*)(d4r_lds + {offs[(g, k)]}))[i] = o_[{k}];')
fill.append(' }')
fill += [' }',
' bar_sync();',
' *ox0 = fx0; *oy0 = fy0; *ow = w; *oh = h;',
'}']
for p in sum((info['params'] for info in group_info.values()), []):
if p != '%f1':
raise CtileErr('only %f1 is supported as a conversion parameter')
return prelude, fill, summary
+234
View File
@@ -0,0 +1,234 @@
"""If-conversion of pure branch diamonds in PTX (used by ptx2hip.py --ifconv).
NVIDIA's DLSS kernels branch around small pure arithmetic alternatives (e.g. the inverse tonemap
`v < 0.998 ? 1 / (1 - v) : (exp2(...) - 1) / (9 v)`), laid out as
@%p bra $A; (or @!%p)
bra.uni $B;
$A: <pure ops> bra.uni $M;
$B: <pure ops> (falls through)
$M:
Branches like these split the code into basic blocks that no compiler schedules memory operations across, so
independent fetch chains on either side are serialized. This pass rewrites such a diamond into straight-line code:
both arms compute into fresh registers and every register an arm assigns is then selected by the predicate. Each lane
keeps exactly the values of the arm it would have taken (GPU arithmetic has no traps), so results are bit-identical.
Only arms made of register arithmetic are converted (no memory, texture, barrier, shuffle, branch or return; no
integer division, which is undefined behaviour in C for some inputs).
"""
import re
REG = re.compile(r'%([a-zA-Z]+)(\d+)\b')
PURE_BASES = {'add', 'sub', 'mul', 'fma', 'mad', 'div', 'rcp', 'sqrt', 'rsqrt', 'ex2', 'lg2', 'min', 'max', 'neg',
'abs', 'cvt', 'mov', 'selp', 'setp', 'and', 'or', 'xor', 'not', 'shl', 'shr', 'set', 'bfi', 'copysign',
'sin', 'cos', 'cvta'}
SEL = {'f': 'selp.f32', 'r': 'selp.b32', 'rd': 'selp.b64', 'rs': 'selp.b16'}
def _pure(it):
if it['kind'] != 'stmt' or it['pred'] is not None:
return False
op = it['op']
base = op.split('.')[0]
if base not in PURE_BASES:
return False
if base == 'div' and not op.endswith('.f32') and 'approx' not in op:
return False # integer division (and IEEE f32 division, which the translator rejects anyway)
if base == 'cvta':
return False
return True
def _target(it):
if it['kind'] == 'stmt' and it['op'].startswith('bra'):
return it['rest'].strip().rstrip(';').strip()
return None
def apply(body, tokenize, parse, split_operands):
items = parse(body, tokenize)
# label -> number of branches targeting it
refs = {}
for it in items:
t = _target(it)
if t:
refs[t] = refs.get(t, 0) + 1
# fresh register numbers per kind
top = {}
for m in REG.finditer(body):
k, n = m.group(1), int(m.group(2))
top[k] = max(top.get(k, 0), n)
fresh = {k: v + 100000 for k, v in top.items()}
def new_reg(kind):
fresh[kind] = fresh.get(kind, 100000) + 1
return f'%{kind}{fresh[kind]}'
use_sites = {}
for x in items:
if x['kind'] == 'stmt':
for r in x['src']:
use_sites.setdefault(r, []).append(id(x))
def uses_outside(r, arm_ids):
return any(u not in arm_ids for u in use_sites.get(r, []))
defined_before = set()
out, i, n_conv = [], 0, 0
while i < len(items):
it = items[i]
conv = None
if it['kind'] == 'stmt' and it['op'].startswith('bra') and it['pred'] is not None:
conv = _match(items, i, refs)
if conv is None:
if it['kind'] == 'stmt':
defined_before.update(it['dst'])
out.append(_text(it))
i += 1
continue
neg, pred, arm_a, arm_b, merge_idx = conv
lines, last = [], [{}, {}]
for side, arm in enumerate((arm_a, arm_b)):
ren = {}
for st in arm:
op, rest = st['text'].split(None, 1)
ops = split_operands(rest)
if len(st['dst']) != 1 or ops[0] != st['dst'][0]:
raise ValueError('unexpected destination form: ' + st['text'])
srcs = [REG.sub(lambda mm: ren.get(mm.group(0), mm.group(0)), o) for o in ops[1:]]
d = st['dst'][0]
kind = REG.match(d).group(1)
if kind not in SEL:
raise ValueError('predicate assigned in a diamond arm: ' + st['text'])
nr = new_reg(kind)
ren[d] = nr
lines.append(f"{op} {', '.join([nr] + srcs)}")
last[side] = ren
# only registers read outside the two arms need a select (arm temporaries are dropped)
arm_ids = {id(x) for x in arm_a} | {id(x) for x in arm_b}
assigned = sorted(r for r in set(last[0]) | set(last[1]) if uses_outside(r, arm_ids))
cond = pred
for r in assigned:
kind = REG.match(r).group(1)
a = last[0].get(r, r if r in defined_before else last[1].get(r))
b = last[1].get(r, r if r in defined_before else last[0].get(r))
# selp d, a, b, p: d = p ? a : b ; arm A is taken when (p xor neg)
if neg:
a, b = b, a
lines.append(f'{SEL[kind]} {r}, {a}, {b}, {cond}')
defined_before.update(assigned)
out.extend(l + ';' for l in lines)
n_conv += 1
i = merge_idx # the merge label itself is emitted by the main loop
return '\n'.join(out), n_conv
def _text(it):
if it['kind'] == 'open':
return '{'
if it['kind'] == 'close':
return '}'
if it['kind'] == 'label':
return it['text'] + ':'
return it['text'] + ';'
def _match(items, i, refs):
"""@p bra A; bra.uni B; A: ... bra.uni M; B: ... M: -> (neg, pred, armA, armB, index of M)"""
br = items[i]
m = re.match(r'@(!?)(%p\d+)\s', br['text'])
if not m:
return None
neg, pred = m.group(1) == '!', m.group(2)
a_lab = _target(br)
if i + 2 >= len(items):
return None
j = items[i + 1]
if not (j['kind'] == 'stmt' and j['op'].startswith('bra') and j['pred'] is None):
return None
b_lab = _target(j)
if refs.get(a_lab) != 1 or refs.get(b_lab) != 1:
return None
def arm(start, label):
if not (items[start]['kind'] == 'label' and items[start]['text'] == label):
return None
body, k = [], start + 1
while k < len(items):
x = items[k]
if x['kind'] == 'label':
return body, k, None
if x['kind'] in ('open', 'close'):
return None
if x['kind'] == 'stmt' and x['op'].startswith('bra'):
if x['pred'] is not None:
return None
return body, k + 1, _target(x)
if not _pure(x):
return None
body.append(x)
k += 1
return None
first = arm(i + 2, a_lab)
order = 'AB'
if first is None:
first = arm(i + 2, b_lab)
order = 'BA'
if first is None:
return None
body1, k, jump1 = first
if jump1 is None:
return None
second = arm(k, b_lab if order == 'AB' else a_lab)
if second is None:
return None
body2, k2, jump2 = second
if jump2 is not None and jump2 != jump1:
return None # the second arm may also jump to the merge label explicitly
if not (k2 < len(items) and items[k2]['kind'] == 'label' and items[k2]['text'] == jump1):
return None
arm_a, arm_b = (body1, body2) if order == 'AB' else (body2, body1)
return neg, pred, arm_a, arm_b, k2
def sink_stores(body, tokenize, parse):
"""Drop labels nothing branches to, then move each st.shared / st.local down to just before the next statement
that could observe or reorder it: a shared/local load or any other store, a barrier, shuffle, atomic, label,
branch or return, or a statement that writes one of the store's registers. Stores keep their relative order, so
every memory location sees the same sequence of writes; texture loads (read-only data) and arithmetic move ahead
of them, which lets independent fetch chains issue together. Returns (body, number of stores moved)."""
items = parse(body, tokenize)
refs = set()
for it in items:
t = _target(it)
if t:
refs.add(t)
items = [it for it in items if not (it['kind'] == 'label' and it['text'] not in refs)]
def is_fence(it):
if it['kind'] != 'stmt':
return True # labels and scope braces
op = it['op']
base = op.split('.')[0]
if base in ('bar', 'shfl', 'atom', 'red', 'bra', 'ret', 'membar', 'fence', 'st', 'sust', 'd4r'):
return True
if base == 'ld' and ('.shared' in op or '.local' in op):
return True
return False
moved = 0
i = len(items) - 1
while i >= 0:
it = items[i]
if it['kind'] == 'stmt' and it['pred'] is None and it['op'].startswith('st.') and ('.shared' in it['op'] or '.local' in it['op']):
regs = set(it['src'])
j = i + 1
while j < len(items) and not is_fence(items[j]) and not (regs & set(items[j]['dst'])):
j += 1
if j > i + 1:
# after the pop the fence that stopped the scan is at j - 1: the store goes right before it
items.insert(j - 1, items.pop(i))
moved += 1
i -= 1
return '\n'.join(_text(it) for it in items), moved
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""in_fuse.py IN.hip > in_unit.h: the hiluma input kernel (translated by ptx2hip.py) as a device function
in_unit(KARGP, gx, gy, out[4])
computing what thread (gx, gy) of the original launch (blocks of 16x16) writes: the 4 halves at network pixel
(gx >> 2, gy), channels 4 (gx & 3) .. 4 (gx & 3) + 3 (used by in_regs.hip).
Changes, all value-preserving:
- thread ids: workgroup/workitem ids -> gx >> 4, gx & 15, gy >> 4, gy & 15 (the kernel only uses gx = 16 wg_x + wi_x,
gy = 16 wg_y + wi_y and gx & 1, gx & 2);
- the per-thread LDS scratch (4 planes of 3 floats at slot 12 * (16 wi_y + wi_x), written in straight-line code
before any branch, read at fixed offsets and once at plane r14 in 0..3) -> 12 registers + a 4-way select;
- the 32-byte local depot (4 float pairs written in straight-line code, one pair read at plane r14) -> 8 registers
+ a 4-way select;
- the 4 final f16 stores -> out[0..3].
Refuses any input that does not match these patterns exactly.
"""
import re
import sys
src = open(sys.argv[1]).read()
lines = src.split('\n')
k0 = next(i for i, l in enumerate(lines) if l.startswith('extern "C" __attribute__((global))'))
assert lines[k0 + 1] == '{'
end = len(lines) - 1
while lines[end].strip() != '}':
end -= 1
body = lines[k0 + 2:end]
first_label = next(i for i, l in enumerate(body) if re.match(r'^L__BB0_\d+:;$', l))
out = []
SUBS = {
'r39 = (uint32_t)((uint32_t)__builtin_amdgcn_workgroup_id_x());': 'r39 = gx >> 4;',
'r41 = (uint32_t)((uint32_t)__builtin_amdgcn_workitem_id_x());': 'r41 = gx & 15u;',
'r46 = (uint32_t)((uint32_t)__builtin_amdgcn_workgroup_id_y());': 'r46 = gy >> 4;',
'r48 = (uint32_t)((uint32_t)__builtin_amdgcn_workitem_id_y());': 'r48 = gy & 15u;',
'r191 = (uint32_t)((uint32_t)__builtin_amdgcn_workitem_id_y());': 'r191 = 0u;',
'r190 = (uint32_t)((uint32_t)__builtin_amdgcn_workitem_id_x());': 'r190 = 0u;',
'r90 = (uint32_t)((uint32_t)(uintptr_t)(AS3 uint8_t*)sharedWarpedSpaceHistoryYcocg);': 'r90 = 0u; // (LDS scratch replaced by registers)',
}
seen = set()
ST = re.compile(r'^ \*\(AS3 float\*\)\(uintptr_t\)\(r20 \+ (\d+)u\) = \(float\)\((f\d+)\);$')
LD = re.compile(r'^ (f\d+) = \*\(AS3 float\*\)\(uintptr_t\)\(r20 \+ (\d+)u\);$')
LD153 = re.compile(r'^ (f\d+) = \*\(AS3 float\*\)\(uintptr_t\)\(r153 \+ 0u\);$')
STORE = re.compile(r'^ \*\(uint16_t\*\)\(uintptr_t\)\(rd48 \+ (\d)\) = \(uint16_t\)\((rs\d+)\);$')
stored = set()
nst = 0
# the 32-byte local depot: 4 float pairs (offsets 0..28) written through rd6 / rd59 / rd61 = depot + 0 in
# straight-line code, read once as the pair at rd61 + 8 r14 (r14 in 0..3) -> 8 registers + a 4-way select
DST = re.compile(r'^ \*\(AS5 float\*\)\(uintptr_t\)\(uint32_t\)\((rd6|rd59|rd61) \+ (\d+)\) = \(float\)\((f\d+)\);$')
DLD = re.compile(r'^ (f\d+) = \*\(AS5 float\*\)\(uintptr_t\)\(uint32_t\)\(rd44 \+ ([04])\);$')
dstored = set()
for i, l in enumerate(body):
t = l.strip()
if t.startswith('const AS4 uint8_t* KARGP =') or t.startswith('const uint64_t KARGU ='):
continue
if t in SUBS:
out.append(' ' + SUBS[t])
seen.add(t)
continue
m = ST.match(l)
if m:
off = int(m.group(1))
assert i < first_label and off % 4 == 0 and off // 3072 < 4 and (off % 3072) // 4 < 3, l
out.append(f' shv{off // 3072}_{(off % 3072) // 4} = {m.group(2)};')
stored.add(off)
continue
m = LD.match(l)
if m:
off = int(m.group(2))
assert off in stored, f'load before store: {l}'
out.append(f' {m.group(1)} = shv{off // 3072}_{(off % 3072) // 4};')
continue
m = LD153.match(l)
if m:
assert all(o in stored for o in (0, 3072, 6144, 9216)), 'plane loads need all planes stored'
out.append(f' {m.group(1)} = r14 == 0u ? shv0_0 : r14 == 1u ? shv1_0 : r14 == 2u ? shv2_0 : shv3_0; // r14 in 0..3')
continue
m = DST.match(l)
if m:
off = int(m.group(2))
assert i < first_label and off % 4 == 0 and off < 32, l
out.append(f' dpv{off // 8}_{(off % 8) // 4} = {m.group(3)};')
dstored.add(off)
continue
m = DLD.match(l)
if m:
assert len(dstored) == 8, 'depot read before all pairs are stored'
j = int(m.group(2)) // 4
out.append(f' {m.group(1)} = r14 == 0u ? dpv0_{j} : r14 == 1u ? dpv1_{j} : r14 == 2u ? dpv2_{j} : dpv3_{j}; // r14 in 0..3')
continue
m = STORE.match(l)
if m:
out.append(f' out[{int(m.group(1)) // 2}] = {m.group(2)};')
nst += 1
continue
assert 'AS3' not in l and 'workitem_id' not in l and 'workgroup_id' not in l, f'unhandled: {l}'
assert not re.search(r'\(rd48 \+', l), f'unhandled store: {l}'
out.append(l)
assert len(seen) == len(SUBS), set(SUBS) - seen
assert nst == 4, nst
assert len(stored) == 12, sorted(stored)
assert len(dstored) == 8, sorted(dstored)
for k in (' rd6 = (uint64_t)(l2_SPL + (uint64_t)0ull);', ' rd59 = (uint64_t)(l2_SPL + (uint64_t)0ull);',
' rd61 = (uint64_t)(l2_SPL + (uint64_t)0ull);', ' rd43 = (uint64_t)((int64_t)(int32_t)r14 * (int64_t)(int32_t)8);',
' rd44 = (uint64_t)(rd61 + rd43);'):
assert k in body, k
# r14 must be 2 r58 + (1 - r65) with r58, r65 parity bits (checked textually)
assert ' r14 = (uint32_t)(r68 + r67);' in body and ' r68 = (uint32_t)(r58 << (1u & 31));' in body
assert ' r67 = (uint32_t)(r26 - r65);' in body and ' r26 = (uint32_t)(1u);' in body
hdr = ['// Generated by kernels/native/in_fuse.py from ' + sys.argv[1].split('/')[-1] + ' (do not edit).',
'#pragma once',
'#include "native_rt.h"',
'// Thread (gx, gy) of hiluma_engine_input_depthinv_mvhi_hdr_v2_rel (16x16 blocks): the 4 halves it stores at',
'// network pixel (gx >> 2, gy), channels 4 (gx & 3) ... KARGP: the input kernel\'s 248-byte parameter block.',
'__attribute__((device)) inline __attribute__((always_inline)) void in_unit(const AS4 uint8_t* KARGP, uint32_t gx, uint32_t gy, uint16_t out[4])',
'{',
' const uint64_t KARGU = (uint64_t)(uintptr_t)KARGP;',
' float ' + ', '.join(f'shv{p}_{j}' for p in range(4) for j in range(3)) + ';',
' float ' + ', '.join(f'dpv{p}_{j}' for p in range(4) for j in range(2)) + ';']
print('\n'.join(hdr + out + ['}']))
+22
View File
@@ -0,0 +1,22 @@
// hiluma input kernel rebuilt from in_unit.h (kernels/native/in_fuse.py on in_ifs.hip): the translated kernel's
// per-thread LDS scratch and 32-byte local array live in registers (the local array compiled to scratch-memory
// flat stores/loads; -21 % in the replay bench). Same launch (16x16 blocks), same output.
// Build: kernels/build.sh (generates in_unit.h from the extracted PTX); by hand: WAVE=32 build_native.sh in_regs.hip OUT.hsaco
#include "in_unit.h"
struct __attribute__((aligned(8))) hiluma_engine_input_depthinv_mvhi_hdr_v2_rel_params { uint8_t b[248]; };
extern "C" __attribute__((global)) __attribute__((amdgpu_flat_work_group_size(1, 256))) void hiluma_engine_input_depthinv_mvhi_hdr_v2_rel(hiluma_engine_input_depthinv_mvhi_hdr_v2_rel_params)
{
const AS4 uint8_t* KARGP = (const AS4 uint8_t*)__builtin_amdgcn_kernarg_segment_ptr();
const uint32_t gx = __builtin_amdgcn_workgroup_id_x() * 16u + __builtin_amdgcn_workitem_id_x();
const uint32_t gy = __builtin_amdgcn_workgroup_id_y() * 16u + __builtin_amdgcn_workitem_id_y();
uint16_t o[4];
in_unit(KARGP, gx, gy, o);
// the original's store: network pixel (gx >> 2, gy), channels 4 (gx & 3) .. + 3; row stride W (u32 at +64),
// buffer at +240 (16 halves per pixel)
const uint32_t W = *(const AS4 uint32_t*)(KARGP + 64);
uint16_t* d = (uint16_t*)*(const AS4 uint64_t*)(KARGP + 240) + ((size_t)(W * gy + (gx >> 2)) * 16 + 4 * (gx & 3));
d[0] = o[0];
d[1] = o[1];
d[2] = o[2];
d[3] = o[3];
}
+28
View File
@@ -0,0 +1,28 @@
// M texture-translation helpers. Kept separate from K runtime code.
#pragma once
#include "ptx_rt.h"
extern "C" __attribute__((device)) h1 __ocml_tanh_f16(h1);
extern "C" __attribute__((device)) h2 m_min2(h2, h2) __asm("llvm.minimumnum.v2f16");
extern "C" __attribute__((device)) h2 m_max2(h2, h2) __asm("llvm.maximumnum.v2f16");
RT uint32_t prmt32(uint32_t a, uint32_t b, uint32_t select)
{
uint32_t out = 0;
#pragma unroll
for (int i = 0; i < 4; ++i) {
uint32_t s = (select >> (4 * i)) & 15u;
uint32_t byte = ((s & 4u) ? b : a) >> (8 * (s & 3u));
byte = (s & 8u) ? ((byte & 128u) ? 255u : 0u) : (byte & 255u);
out |= byte << (8 * i);
}
return out;
}
RT uint32_t shfl_idx(uint32_t input, uint32_t index, uint32_t opts, bool* in_bounds)
{
uint32_t mask = (opts >> 8) & 31u;
uint32_t subsection = laneid() & mask;
uint32_t end = subsection | (opts & ~mask & 31u);
uint32_t src = subsection | (index & ~mask & 31u);
bool valid = src <= end;
if (in_bounds) *in_bounds = valid;
return __builtin_amdgcn_ds_bpermute(4u * (warp_base() | (valid ? src : laneid())), input);
}
+185
View File
@@ -0,0 +1,185 @@
// Runtime for natively compiled DLSS texture kernels: PTX semantics (ptx_rt.h) plus surface stores
// (tex/tex_common.h, with ZLUDA's generic raw-store fallback ported here, since no ZLUDA bitcode is linked).
#pragma once
#include "ptx_rt.h"
#include "../tex/tex_common.h"
extern "C" __attribute__((device)) v4f __ockl_image_load_2D(tsharp_t* image, v2i coord);
namespace zsurf {
// zluda_ptx_impl.cpp surface_* helpers (raw sust.b stores), verbatim semantics
RT int channel_bytes(int dt)
{
switch (dt)
{
case 0: case 2: case 8: case 11: return 1;
case 1: case 3: case 9: case 12: case 14: return 2;
case 10: case 13: case 15: return 4;
default: return 0;
}
}
RT int channel_count(int order)
{
switch (order)
{
case 3: case 4: case 5: return 2;
case 6: case 12: return 3;
case 7: case 8: case 9: case 10: case 11: case 13: case 14: case 15: return 4;
default: return 1;
}
}
RT int memory_channel_lane(int order, int channel)
{
switch (order)
{
case 0: return 3;
case 5: return channel == 0 ? 0 : 3;
case 9: case 15: return channel == 0 ? 2 : channel == 2 ? 0 : channel;
case 10: return channel == 0 ? 3 : channel - 1;
case 11: return 3 - channel;
default: return channel;
}
}
RT uint32_t float_to_half_bits(float value)
{
uint32_t bits = __builtin_bit_cast(uint32_t, value);
uint32_t sign = (bits >> 16) & 0x8000u, exponent = (bits >> 23) & 0xffu, mantissa = bits & 0x7fffffu;
if (exponent == 0xff)
return sign | 0x7c00u | (mantissa != 0 ? 0x200u | (mantissa >> 13) : 0u);
int half_exponent = int(exponent) - 112;
if (half_exponent >= 31)
return sign | 0x7c00u;
uint32_t shift = 13;
if (half_exponent <= 0)
{
if (half_exponent < -10)
return sign;
mantissa |= 0x800000u;
shift = uint32_t(14 - half_exponent);
half_exponent = 0;
}
uint32_t half_mantissa = mantissa >> shift, remainder = mantissa & ((1u << shift) - 1u), halfway = 1u << (shift - 1u);
uint32_t result = sign | (uint32_t(half_exponent) << 10);
result += half_mantissa & (half_exponent == 0 ? 0x7ffu : 0x3ffu);
if (remainder > halfway || (remainder == halfway && (half_mantissa & 1u)))
result++;
return result;
}
RT int32_t clampi(int32_t v, int32_t lo, int32_t hi) { return v < lo ? lo : (v > hi ? hi : v); }
RT float decode(uint32_t bits, int dt)
{
switch (dt)
{
case 0: return float(clampi(int32_t(int8_t(bits)), -127, 127)) / 127.0f;
case 1: return float(clampi(int32_t(int16_t(bits)), -32767, 32767)) / 32767.0f;
case 2: return float(int32_t(bits & 0xffu)) / 255.0f;
case 3: return float(int32_t(bits & 0xffffu)) / 65535.0f;
case 8: return __builtin_bit_cast(float, int32_t(int8_t(bits)));
case 9: return __builtin_bit_cast(float, int32_t(int16_t(bits)));
case 14: return half_bits_to_float(bits);
default: return __builtin_bit_cast(float, bits);
}
}
RT uint32_t encode(float value, int dt)
{
switch (dt)
{
case 0: return uint32_t(clampi(int32_t(__builtin_floorf(value * 127.0f + 0.5f)), -127, 127)) & 0xffu;
case 1: return uint32_t(clampi(int32_t(__builtin_floorf(value * 32767.0f + 0.5f)), -32767, 32767)) & 0xffffu;
case 2: return uint32_t(clampi(int32_t(__builtin_floorf(value * 255.0f + 0.5f)), 0, 255));
case 3: return uint32_t(clampi(int32_t(__builtin_floorf(value * 65535.0f + 0.5f)), 0, 65535));
case 14: return float_to_half_bits(value);
default: return __builtin_bit_cast(uint32_t, value);
}
}
RT int lane_channel(int order, int lane, int channels)
{
switch (order)
{
case 0: return lane == 3 ? 0 : -1;
case 5: return lane == 0 ? 0 : (lane == 3 ? 1 : -1);
case 9: case 15: return lane == 0 ? 2 : (lane == 2 ? 0 : lane);
case 10: return lane == 3 ? 0 : lane + 1;
case 11: return 3 - lane;
default: return lane < channels ? lane : -1;
}
}
RT uint32_t sel_channel(uint32_t c0, uint32_t c1, uint32_t c2, uint32_t c3, int ch)
{
return ch == 0 ? c0 : (ch == 1 ? c1 : (ch == 2 ? c2 : c3));
}
RT float sel_lane(v4f v, int lane) { return lane == 0 ? v.x : (lane == 1 ? v.y : (lane == 2 ? v.z : v.w)); }
RT uint32_t merge_channel(uint32_t bits, int channel, int cb, uint32_t data, int byte_offset, int byte_count)
{
for (int b = 0; b < 4; b++)
{
int relative = channel * cb + b - byte_offset;
if (b < cb && relative >= 0 && relative < byte_count)
{
uint32_t byte = (data >> (8 * relative)) & 0xffu;
bits = (bits & ~(0xffu << (8 * b))) | (byte << (8 * b));
}
}
return bits;
}
RT float lane_value(uint32_t c0, uint32_t c1, uint32_t c2, uint32_t c3, int order, int lane, int channels, int dt)
{
int ch = lane_channel(order, lane, channels);
return ch < 0 ? 0.0f : decode(sel_channel(c0, c1, c2, c3, ch), dt);
}
__attribute__((device, noinline)) static void store_raw_2d(tsharp_t* image, int32_t cx, int32_t cy, uint32_t data, int byte_count)
{
int dt = __ockl_image_channel_data_type_2D(image), order = __ockl_image_channel_order_2D(image);
int cb = channel_bytes(dt);
if (cb == 0 || cx < 0)
return;
int channels = channel_count(order), pixel_bytes = cb * channels;
for (int consumed = 0; consumed < byte_count;)
{
int byte_x = cx + consumed, pixel_x = byte_x / pixel_bytes, byte_offset = byte_x - pixel_x * pixel_bytes;
int chunk = pixel_bytes - byte_offset;
if (chunk > byte_count - consumed)
chunk = byte_count - consumed;
uint32_t fragment = data >> (8 * consumed);
uint32_t c0 = 0, c1 = 0, c2 = 0, c3 = 0;
if (byte_offset != 0 || chunk < pixel_bytes)
{
v4f loaded = __ockl_image_load_2D(image, (v2i){pixel_x, cy});
c0 = encode(sel_lane(loaded, memory_channel_lane(order, 0)), dt);
c1 = encode(sel_lane(loaded, memory_channel_lane(order, 1)), dt);
c2 = encode(sel_lane(loaded, memory_channel_lane(order, 2)), dt);
c3 = encode(sel_lane(loaded, memory_channel_lane(order, 3)), dt);
}
c0 = merge_channel(c0, 0, cb, fragment, byte_offset, chunk);
c1 = merge_channel(c1, 1, cb, fragment, byte_offset, chunk);
c2 = merge_channel(c2, 2, cb, fragment, byte_offset, chunk);
c3 = merge_channel(c3, 3, cb, fragment, byte_offset, chunk);
__ockl_image_store_2D(image, (v2i){pixel_x, cy},
(v4f){lane_value(c0, c1, c2, c3, order, 0, channels, dt), lane_value(c0, c1, c2, c3, order, 1, channels, dt),
lane_value(c0, c1, c2, c3, order, 2, channels, dt), lane_value(c0, c1, c2, c3, order, 3, channels, dt)});
consumed += chunk;
}
}
} // namespace zsurf
extern "C" __attribute__((device)) void __zluda_ptx_impl_surfobj_b_2d_b32_zero(uint64_t surface, v2i coord, uint32_t value)
{
zsurf::store_raw_2d((tsharp_t*)(uintptr_t)surface, coord.x, coord.y, value, 4);
}
extern "C" __attribute__((device)) void __zluda_ptx_impl_surfobj_b_2d_v2_b16_zero(uint64_t surface, v2i coord, v2s value)
{
zsurf::store_raw_2d((tsharp_t*)(uintptr_t)surface, coord.x, coord.y, uint32_t(uint16_t(value.x)) | (uint32_t(uint16_t(value.y)) << 16), 4);
}
// A sample that every lane takes with the same handle, coordinates and lod (e.g. a 1x1 exposure texture at (0.5,
// 0.5)): only the first active lane samples and the result is broadcast with readfirstlane. Same values, but the
// texture unit processes one lane instead of the whole wave.
RT f4 tex_level_uniform(uint64_t t, float x, float y, float lod)
{
f4 r = (f4){0.0f, 0.0f, 0.0f, 0.0f};
if (wave_lane() == (uint32_t)__builtin_ctzll(__builtin_amdgcn_read_exec()))
r = tex_level(t, x, y, lod);
return (f4){asf(__builtin_amdgcn_readfirstlane(asu(r[0]))), asf(__builtin_amdgcn_readfirstlane(asu(r[1]))),
asf(__builtin_amdgcn_readfirstlane(asu(r[2]))), asf(__builtin_amdgcn_readfirstlane(asu(r[3])))};
}
+71
View File
@@ -0,0 +1,71 @@
// Structured path-B tile fill. Same color/depth values and 24-column LDS layout
// as the original kernel; work-items traverse x/y directly without integer division.
#pragma once
#ifndef D4R_OUTPUT_TILE_PITCH
#define D4R_OUTPUT_TILE_PITCH 24
#endif
static_assert(D4R_OUTPUT_TILE_PITCH >= 24 && D4R_OUTPUT_TILE_PITCH <= 32, "LDS tile pitch capacity");
RT void output_color_pack(f4 color, float exposure, uint32_t* a, uint32_t* b)
{
const float r = fmaxn(color[0], 0.0f), g = fmaxn(color[1], 0.0f), blue = fmaxn(color[2], 0.0f);
const float qr = r * 0.25f, hg = g * 0.5f, qb = blue * 0.25f;
const float y = exposure * ((qr + hg) + qb);
const float co = __builtin_fmaf(r, 0.5f, g * 0.0f) - blue * 0.5f;
const float cg = (hg - qr) - qb;
float weight;
if (y < 1000.0f)
weight = rcp_a(y + 1.0f);
else
{
const float z = __builtin_fmaf(y, 9.0f, 1.0f);
const float ln = lg2_a(z) * asf(0x3F317218u);
const float q = div_a(ln, 9.0f) + asf(0xBC4FAE29u);
weight = rcp_a(y) * q;
}
*a = pack16(ashu((h1)(y * weight)), ashu((h1)((exposure * co) * weight)));
*b = pack16(ashu((h1)((exposure * cg) * weight)), ashu((h1)color[3]));
}
RT int output_tile_coord(int relative, int lo, int hi)
{
return relative > 0 ? __builtin_elementwise_min(lo + relative, hi) : lo;
}
RT void output_tile_fill(const AS4 uint8_t* args, int x0, int y0, int width, int height,
float exposure, AS3 uint8_t* color_tile, AS3 uint8_t* depth_tile)
{
const uint64_t color = *(const AS4 uint64_t*)(args + 200);
const uint64_t depth = *(const AS4 uint64_t*)(args + 248);
const int cx0 = *(const AS4 int32_t*)(args + 208), cy0 = *(const AS4 int32_t*)(args + 212);
const int cx1 = *(const AS4 int32_t*)(args + 216), cy1 = *(const AS4 int32_t*)(args + 220);
const int dx0 = *(const AS4 int32_t*)(args + 96), dy0 = *(const AS4 int32_t*)(args + 100);
const int dx1 = *(const AS4 int32_t*)(args + 104), dy1 = *(const AS4 int32_t*)(args + 108);
bool fill_depth = true;
#ifdef D4R_K_NO_DEPTH_TILE
// Pinned K LUT has |slope|<=1. Its 4x3 color stencil stays in the four-texel
// halo for positive upsampling scale<=1, so no color read aliases depth LDS.
// Keep the original fill outside the proven geometry. Input depth is unchanged.
const float scale_x = *(const AS4 float*)(args + 40), scale_y = *(const AS4 float*)(args + 44);
fill_depth = !(scale_x > 0.0f && scale_x <= 1.0f && scale_y > 0.0f && scale_y <= 1.0f);
#endif
// Path B selects a footprint at most 24 x 24. At each iteration 0<=x<width,
// 0<=y<height: color accesses stay within 4608 bytes, depth within 2304 bytes.
for (int y = __builtin_amdgcn_workitem_id_y(); y < height; y += 16)
for (int x = __builtin_amdgcn_workitem_id_x(); x < width; x += 16)
{
const int sx = x0 + x, sy = y0 + y;
const f4 c = tex_fetch(color, output_tile_coord(sx, cx0, cx1), output_tile_coord(sy, cy0, cy1));
uint32_t a, b;
output_color_pack(c, exposure, &a, &b);
AS3 uint32_t* dst = (AS3 uint32_t*)(color_tile + y * (8 * D4R_OUTPUT_TILE_PITCH) + x * 8);
dst[0] = a;
dst[1] = b;
if (fill_depth)
{
const f4 d = tex_fetch(depth, output_tile_coord(sx, dx0, dx1), output_tile_coord(sy, dy0, dy1));
*(AS3 float*)(depth_tile + y * (4 * D4R_OUTPUT_TILE_PITCH) + x * 4) = d[0];
}
}
}
+857
View File
@@ -0,0 +1,857 @@
#!/usr/bin/env python3
"""Translate one NVIDIA PTX kernel (DLSS texture kernels) into native HIP C++.
usage: ptx2hip.py MODULE.ptx KERNEL > KERNEL.hip
The output is a plain HIP kernel with the same name and parameter block. Every PTX instruction becomes
the C expression of ZLUDA's lowering of it (see ptx_rt.h), so the translation computes what ZLUDA's own
compile of the PTX computes, but it is compiled natively by clang and can then be optimised as C++.
Registers become locals, basic blocks become labels, branches become gotos.
"""
import re
import sys
TYPES = {'f': 'float', 'r': 'uint32_t', 'rd': 'uint64_t', 'rs': 'uint16_t', 'p': 'bool', 'fd': 'double',
'SP': 'uint64_t', 'SPL': 'uint64_t'}
SREG = {'%tid.x': '__builtin_amdgcn_workitem_id_x()', '%tid.y': '__builtin_amdgcn_workitem_id_y()',
'%tid.z': '__builtin_amdgcn_workitem_id_z()', '%ctaid.x': '__builtin_amdgcn_workgroup_id_x()',
'%ctaid.y': '__builtin_amdgcn_workgroup_id_y()', '%ctaid.z': '__builtin_amdgcn_workgroup_id_z()',
'%laneid': 'laneid()',
'%ntid.x': '__builtin_amdgcn_workgroup_size_x()', '%ntid.y': '__builtin_amdgcn_workgroup_size_y()',
'%nctaid.x': '(__builtin_amdgcn_grid_size_x() / __builtin_amdgcn_workgroup_size_x())',
'%nctaid.y': '(__builtin_amdgcn_grid_size_y() / __builtin_amdgcn_workgroup_size_y())'}
REG_RE = re.compile(r'%([a-zA-Z]+)(\d+)$')
class Err(Exception):
pass
def strip_comments(text):
return re.sub(r'//[^\n]*', '', text)
def tokenize(body):
"""-> list of ('open'|'close'|'label'|'stmt', text)"""
out, i, n = [], 0, len(body)
while i < n:
c = body[i]
if c.isspace():
i += 1
continue
if c == '{':
out.append(('open', None)); i += 1; continue
if c == '}':
out.append(('close', None)); i += 1; continue
m = re.match(r'(\$?[A-Za-z_][\w$]*):', body[i:])
if m and not body[i:].startswith('%'):
out.append(('label', m.group(1))); i += m.end(); continue
j = body.index(';', i)
out.append(('stmt', ' '.join(body[i:j].split())))
i = j + 1
return out
def split_operands(s):
ops, depth, cur = [], 0, ''
for ch in s:
if ch in '{[':
depth += 1
elif ch in '}]':
depth -= 1
if ch == ',' and depth == 0:
ops.append(cur.strip()); cur = ''
else:
cur += ch
if cur.strip():
ops.append(cur.strip())
return ops
class Translator:
def __init__(self, ptx, kernel):
self.kernel = kernel
text = strip_comments(ptx)
m = re.search(r'\.entry\s+' + re.escape(kernel) + r'\s*\((.*?)\)', text, re.S)
if not m:
raise Err('kernel not found')
params = m.group(1)
pm = re.search(r'\.param\s+\.align\s+(\d+)\s+\.b8\s+(\w+)\[(\d+)\]', params)
self.param_name, self.param_size, self.param_align = pm.group(2), int(pm.group(3)), int(pm.group(1))
rest = text[m.end():]
self.attrs = rest[:rest.index('{')]
start = rest.index('{')
depth, k = 0, start
while True:
if rest[k] == '{':
depth += 1
elif rest[k] == '}':
depth -= 1
if depth == 0:
break
k += 1
self.body = rest[start + 1:k]
self.globals_text = text[:m.start()]
self.regs = {} # c name -> type
self.shared = [] # (name, size, align)
self.locals = []
self.scope_names = [{}]
self.scope_id = 0
self.lines = []
# ------------------------------------------------------------ operands
def reg(self, tok):
tok = tok.strip()
for scope in reversed(self.scope_names):
if tok in scope:
return scope[tok]
m = REG_RE.match(tok)
if not m:
raise Err('not a register: ' + tok)
kind, num = m.groups()
name = f'{kind}{num}'
if kind not in TYPES:
raise Err('register kind ' + kind)
self.regs[name] = TYPES[kind]
return name
def rtype(self, name):
return self.regs[name]
def val(self, tok, as_type):
"""operand as a C expression of type as_type ('f32','u32','s32','u64','s64','u16','s16','pred','f16x2')"""
tok = tok.strip()
if tok in SREG:
return f'(uint32_t){SREG[tok]}'
if tok.startswith('%') or any(tok in s for s in self.scope_names):
name = self.reg(tok)
t = self.rtype(name)
if as_type == 'f32':
return name if t == 'float' else f'asf({name})'
if as_type in ('u32', 'b32'):
return f'asu({name})' if t == 'float' else name
if as_type == 's32':
return f'(int32_t)asu({name})' if t == 'float' else f'(int32_t){name}'
if as_type in ('u64', 'b64'):
return name
if as_type == 's64':
return f'(int64_t){name}'
if as_type in ('u16', 'b16'):
return name
if as_type == 's16':
return f'(int16_t){name}'
if as_type == 'f16':
return f'ash({name})'
if as_type == 'f16x2':
return f'ash2({name})'
if as_type == 'pred':
return name
raise Err('type ' + as_type)
if tok in self.shared_names():
return f'(uint32_t)(uintptr_t)(AS3 uint8_t*){tok}'
if tok == '__local_depot0':
# private (address space 5) address of the depot, zero-extended: ld/st.local use it as an AS5 pointer
return '(uint64_t)(uint32_t)(uintptr_t)(AS5 uint8_t*)__local_depot0'
# immediates
if re.match(r'0[fF][0-9A-Fa-f]{8}$', tok):
bits = int(tok[2:], 16)
return f'asf(0x{bits:08X}u)' if as_type == 'f32' else f'0x{bits:08X}u'
if re.match(r'-?\d+$', tok) or re.match(r'0x[0-9a-fA-F]+$', tok):
v = int(tok, 0)
if as_type == 'f32':
raise Err('int immediate as float')
if as_type in ('u64', 's64', 'b64'):
return f'(uint64_t){v}ull' if v >= 0 else f'(uint64_t)({v}ll)'
if as_type in ('u16', 'b16', 's16'):
return f'(uint16_t){v & 0xffff}'
if as_type == 's32':
v = v & 0xffffffff
v = v - (1 << 32) if v >= (1 << 31) else v
return f'(int32_t){v}' if v != -(1 << 31) else '(int32_t)0x80000000u'
return f'{v & 0xffffffff}u'
raise Err('operand ' + tok)
def shared_names(self):
return [s[0] for s in self.shared]
def assign(self, dst, expr, src_type):
"""dst = expr, expr of C type src_type ('float','u32','u64','u16','bool','f16x2','f16')"""
name = self.reg(dst)
t = self.rtype(name)
if src_type == 'float':
e = expr if t == 'float' else f'asu({expr})'
elif src_type == 'u32':
e = f'asf({expr})' if t == 'float' else f'(uint32_t)({expr})'
elif src_type == 'u64':
e = f'(uint64_t)({expr})'
elif src_type == 'u16':
e = f'(uint16_t)({expr})'
elif src_type == 'f16':
e = f'ashu({expr})'
elif src_type == 'f16x2':
e = f'ash2u({expr})'
elif src_type == 'bool':
e = f'({expr})'
else:
raise Err('src type')
self.emit(f'{name} = {e};')
def emit(self, s):
self.lines.append(' ' + s)
def addr(self, tok):
"""[reg+off] / [name+off] -> (base_expr, offset_int, space_hint)"""
m = re.match(r'\[\s*([^\]+]+?)\s*(?:\+\s*(-?\d+))?\s*\]$', tok)
if not m:
raise Err('address ' + tok)
base, off = m.group(1), int(m.group(2) or 0)
if base == self.param_name:
return 'KARG', off
if base in self.shared_names():
return f'(uint32_t)(uintptr_t)(AS3 uint8_t*){base}', off
return self.val(base, 'u64' if not base.startswith('%r') or base.startswith('%rd') else 'u32'), off
# ------------------------------------------------------------ instructions
def translate(self):
for kind, text in tokenize(self.body):
if kind == 'open':
self.scope_names.append({})
continue
if kind == 'close':
self.scope_names.pop()
continue
if kind == 'label':
self.lines.append(f'{text.replace("$", "")}:;')
lab = text.replace('$', '')
if lab in getattr(self, 'stamps', []):
self.lines.append(f' D4R_STAMP({self.stamps.index(lab) + 1});')
if getattr(self, 'bbcount', False):
self.bblabels.append(lab)
self.lines.append(f' D4R_BBC({len(self.bblabels) - 1});')
continue
self.stmt(text)
def stmt(self, s):
if s.startswith('.reg'):
m = re.match(r'\.reg\s+\.(\w+)\s+(.*)$', s)
ty, names = m.groups()
if '<' in names:
return # numbered register banks: declared on use
self.scope_id += 1
for nm in names.split(','):
nm = nm.strip()
cname = f'l{self.scope_id}_{nm.lstrip("%")}'
ctype = {'f16': 'uint16_t', 'b16': 'uint16_t', 'f32': 'float', 'b32': 'uint32_t', 'pred': 'bool',
'f16x2': 'uint32_t', 'u32': 'uint32_t', 's32': 'uint32_t', 'b64': 'uint64_t', 'u64': 'uint64_t'}[ty]
self.scope_names[-1][nm] = cname
self.regs[cname] = ctype
return
if s.startswith('.shared'):
m = re.match(r'\.shared\s+\.align\s+(\d+)\s+\.b8\s+(\w+)\[(\d+)\]', s)
self.shared.append((m.group(2), int(m.group(3)), int(m.group(1))))
return
if s.startswith('.local'):
m = re.match(r'\.local\s+\.align\s+(\d+)\s+\.b8\s+(\w+)\[(\d+)\]', s)
self.locals.append((m.group(2), int(m.group(3)), int(m.group(1))))
return
if s.startswith('.pragma'):
return
pred = None
m = re.match(r'@(!?)(%p\d+)\s+(.*)$', s)
if m:
pred = ('!' if m.group(1) else '') + self.reg(m.group(2))
s = m.group(3)
parts = s.split(None, 1)
op = parts[0]
args = split_operands(parts[1]) if len(parts) > 1 else []
if pred is not None:
if op.startswith('bra'):
self.emit(f'if ({pred}) goto {args[0].replace("$", "")};')
return
self.emit(f'if ({pred}) {{')
self.op(op, args)
self.emit('}')
return
self.op(op, args)
def op(self, op, a):
o = op.split('.')
base = o[0]
mods = set(o[1:])
E, A = self.emit, self.assign
V = self.val
def ftype():
for t in ('f32', 'f16x2', 'f16', 'f64'):
if t in mods:
return t
return None
if base == 'bra':
E(f'goto {a[0].replace("$", "")};'); return
if base == 'ret':
if getattr(self, 'stamps', None):
E(f'D4R_STAMP({len(self.stamps) + 1});')
E('return;'); return
if base == 'bar':
E('bar_sync();'); return
if base == 'mov':
dst, src = a
if dst.startswith('{'):
names = [x.strip() for x in dst[1:-1].split(',')]
srcv = V(src, 'u32')
A(names[0], f'{srcv} & 0xffffu', 'u16')
A(names[1], f'{srcv} >> 16', 'u16')
return
if src.startswith('{'):
names = [x.strip() for x in src[1:-1].split(',')]
if 'b64' in mods:
A(dst, f'(uint64_t){V(names[0], "u32")} | ((uint64_t){V(names[1], "u32")} << 32)', 'u64'); return
A(dst, f'pack16({V(names[0], "u16")}, {V(names[1], "u16")})', 'u32'); return
if src == self.param_name:
A(dst, 'KARGU', 'u64'); return
if 'f32' in mods:
A(dst, V(src, 'f32'), 'float'); return
if mods & {'u64', 'b64', 's64'}:
A(dst, V(src, 'u64'), 'u64'); return
if mods & {'u16', 'b16', 's16'}:
A(dst, V(src, 'u16'), 'u16'); return
A(dst, V(src, 'u32'), 'u32'); return
ft = ftype()
# ---------------------------------------------------- float arithmetic
if ft == 'f32' and base in ('add', 'sub', 'mul'):
if 'rz' in mods:
raise Err('add.rz outside the roundf idiom')
sym = {'add': '+', 'sub': '-', 'mul': '*'}[base]
A(a[0], f'{V(a[1], "f32")} {sym} {V(a[2], "f32")}', 'float'); return
if ft == 'f32' and base == 'fma':
A(a[0], f'__builtin_fmaf({V(a[1], "f32")}, {V(a[2], "f32")}, {V(a[3], "f32")})', 'float'); return
if ft == 'f32' and base in ('max', 'min'):
if 'NaN' in mods:
raise Err('NaN min/max')
A(a[0], f'{"fmaxn" if base == "max" else "fminn"}({V(a[1], "f32")}, {V(a[2], "f32")})', 'float'); return
if ft == 'f32' and base == 'neg':
A(a[0], f'neg_ftz({V(a[1], "f32")})' if 'ftz' in mods else f'-{V(a[1], "f32")}', 'float'); return
if ft == 'f32' and base == 'abs':
A(a[0], f'abs_ftz({V(a[1], "f32")})' if 'ftz' in mods else f'__builtin_fabsf({V(a[1], "f32")})', 'float'); return
if ft == 'f32' and base in ('rcp', 'ex2', 'lg2', 'rsqrt', 'sqrt'):
if 'approx' not in mods:
raise Err(op)
fn = {'rcp': 'rcp_a', 'ex2': 'ex2_a', 'lg2': 'lg2_a', 'rsqrt': 'rsq_a', 'sqrt': '__builtin_amdgcn_sqrtf'}[base]
A(a[0], f'{fn}({V(a[1], "f32")})', 'float'); return
if ft == 'f32' and base == 'div':
if 'approx' not in mods:
raise Err(op)
A(a[0], f'div_a({V(a[1], "f32")}, {V(a[2], "f32")})', 'float'); return
# ---------------------------------------------------- f16 / f16x2
if ft == 'f16x2' and base in ('add', 'sub', 'mul', 'fma'):
x = [V(t, 'f16x2') for t in a[1:]]
e = f'h2fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})'
if 'sat' in mods:
e = f'h2sat({e})'
A(a[0], e, 'f16x2'); return
if ft == 'f16x2' and base == 'abs':
A(a[0], f'h2abs({V(a[1], "f16x2")})', 'f16x2'); return
if ft == 'f16x2' and base == 'ex2':
A(a[0], f'h2ex2({V(a[1], "f16x2")})', 'f16x2'); return
if ft in ('f16', 'f16x2') and base in ('min', 'max'):
if 'NaN' in mods:
raise Err('NaN-propagating half min/max is not supported')
fn = 'hminn' if base == 'min' else 'hmaxn'
if ft == 'f16':
A(a[0], f'{fn}({V(a[1], "f16")}, {V(a[2], "f16")})', 'f16'); return
x, y = V(a[1], 'f16x2'), V(a[2], 'f16x2')
A(a[0], f'(h2){{{fn}({x}.x, {y}.x), {fn}({x}.y, {y}.y)}}', 'f16x2'); return
if ft == 'f16' and base in ('add', 'sub', 'mul', 'fma'):
x = [V(t, 'f16') for t in a[1:]]
e = f'hfma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'(h1)({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})'
if 'sat' in mods:
e = f'hsat({e})'
A(a[0], e, 'f16'); return
if op == 'tanh.approx.f16':
A(a[0], f'__ocml_tanh_f16({V(a[1], "f16")})', 'f16'); return
if base == 'set' and 'f16x2' in mods and 'u32' in mods:
cmp = self.cmp_op(mods)
x, y = V(a[1], 'f16x2'), V(a[2], 'f16x2')
A(a[0], f'pack16(({cmp.format(f"{x}.x", f"{y}.x")}) ? 0xffffu : 0u, ({cmp.format(f"{x}.y", f"{y}.y")}) ? 0xffffu : 0u)', 'u32'); return
# ---------------------------------------------------- compares / selects
if base == 'setp':
cmp = self.cmp_op(mods)
if mods & {'f32'}:
x, y = V(a[1], 'f32'), V(a[2], 'f32')
elif 'f16' in mods:
x, y = V(a[1], 'f16'), V(a[2], 'f16')
elif mods & {'s32'}:
x, y = V(a[1], 's32'), V(a[2], 's32')
elif mods & {'u32', 'b32'}:
x, y = V(a[1], 'u32'), V(a[2], 'u32')
elif mods & {'s16'}:
x, y = V(a[1], 's16'), V(a[2], 's16')
elif mods & {'u16', 'b16'}:
x, y = V(a[1], 'u16'), V(a[2], 'u16')
elif mods & {'s64'}:
x, y = V(a[1], 's64'), V(a[2], 's64')
elif mods & {'u64'}:
x, y = V(a[1], 'u64'), V(a[2], 'u64')
else:
raise Err(op)
if len(a) != 3 or '|' in a[0]:
raise Err('setp form ' + op)
A(a[0], cmp.format(x, y), 'bool'); return
if base == 'selp':
t = 'f32' if 'f32' in mods else 'u16' if mods & {'u16', 'b16', 's16'} else 'u64' if mods & {'u64', 'b64', 's64'} else 'u32'
ct = {'f32': 'float', 'u16': 'u16', 'u64': 'u64', 'u32': 'u32'}[t]
A(a[0], f'{V(a[3], "pred")} ? {V(a[1], t)} : {V(a[2], t)}', ct); return
# ---------------------------------------------------- predicates / integer
if 'pred' in mods and base in ('and', 'or', 'xor', 'not'):
if base == 'not':
A(a[0], f'!{V(a[1], "pred")}', 'bool'); return
sym = {'and': '&&', 'or': '||', 'xor': '!='}[base]
A(a[0], f'{V(a[1], "pred")} {sym} {V(a[2], "pred")}', 'bool'); return
if base in ('and', 'or', 'xor', 'not', 'shl', 'shr', 'add', 'sub', 'mul', 'mad', 'min', 'max', 'div', 'rem', 'neg', 'abs'):
if ft is not None:
raise Err('unhandled floating operation: ' + op)
width = 64 if mods & {'u64', 's64', 'b64'} else 16 if mods & {'u16', 's16', 'b16'} else 32
signed = bool(mods & {'s32', 's64', 's16'})
ut = {64: 'u64', 32: 'u32', 16: 'u16'}[width]
st = {64: 's64', 32: 's32', 16: 's16'}[width]
ct = {64: 'u64', 32: 'u32', 16: 'u16'}[width]
if base == 'mul' and 'hi' in mods and width == 32:
wide = 'int64_t' if signed else 'uint64_t'
typ = 's32' if signed else 'u32'
A(a[0], f'(({wide}){V(a[1], typ)} * ({wide}){V(a[2], typ)}) >> 32', 'u32'); return
if base == 'mul' and 'wide' in mods:
if mods & {'s32'}:
A(a[0], f'(int64_t){V(a[1], "s32")} * (int64_t){V(a[2], "s32")}', 'u64'); return
if mods & {'u32'}:
A(a[0], f'(uint64_t){V(a[1], "u32")} * (uint64_t){V(a[2], "u32")}', 'u64'); return
if mods & {'u16'}:
A(a[0], f'(uint32_t){V(a[1], "u16")} * (uint32_t){V(a[2], "u16")}', 'u32'); return
raise Err(op)
if base == 'not':
A(a[0], f'~{V(a[1], ut)}', ct); return
if base in ('neg', 'abs'):
if base == 'abs' and signed:
A(a[0], f'{V(a[1], st)} < 0 ? (uint{width}_t)(0 - {V(a[1], ut)}) : {V(a[1], ut)}', ct); return
if base == 'neg':
A(a[0], f'(uint{width}_t)(0 - {V(a[1], ut)})', ct); return
raise Err(op)
x = V(a[1], st if signed else ut)
if base == 'shl':
A(a[0], f'{V(a[1], ut)} << ({V(a[2], "u32")} & {width - 1})', ct); return
if base == 'shr':
if signed:
A(a[0], f'{V(a[1], st)} >> ({V(a[2], "u32")} & {width - 1})', ct); return
A(a[0], f'{V(a[1], ut)} >> ({V(a[2], "u32")} & {width - 1})', ct); return
y = V(a[2], st if signed else ut)
if base in ('and', 'or', 'xor'):
sym = {'and': '&', 'or': '|', 'xor': '^'}[base]
A(a[0], f'{V(a[1], ut)} {sym} {V(a[2], ut)}', ct); return
if base in ('add', 'sub'):
A(a[0], f'{V(a[1], ut)} {"+" if base == "add" else "-"} {V(a[2], ut)}', ct); return
if base == 'mul':
if 'lo' not in mods:
raise Err(op)
A(a[0], f'{V(a[1], ut)} * {V(a[2], ut)}', ct); return
if base == 'mad':
if 'lo' not in mods:
raise Err(op)
A(a[0], f'{V(a[1], ut)} * {V(a[2], ut)} + {V(a[3], ut)}', ct); return
if base in ('min', 'max'):
fn = 'min' if base == 'min' else 'max'
A(a[0], f'__builtin_elementwise_{fn}({x}, {y})', ct); return
if base == 'div':
A(a[0], f'{x} / {y}', ct); return
if base == 'rem':
A(a[0], f'{x} % {y}', ct); return
if base == 'bfi':
A(a[0], f'bfi32({V(a[1], "u32")}, {V(a[2], "u32")}, {V(a[3], "u32")}, {V(a[4], "u32")})', 'u32'); return
if op == 'prmt.b32':
A(a[0], f'prmt32({V(a[1], "u32")}, {V(a[2], "u32")}, {V(a[3], "u32")})', 'u32'); return
# ---------------------------------------------------- conversions
if base == 'cvt':
return self.cvt(op, o[1:], a)
if base == 'cvta':
A(a[0], V(a[1], 'u64'), 'u64'); return
# ---------------------------------------------------- memory
if base == 'ld':
return self.ld(o[1:], a)
if base == 'st':
return self.st(o[1:], a)
if base == 'tex':
dst = [x.strip() for x in a[0][1:-1].split(',')]
m = re.match(r'\[\s*(%rd\d+)\s*,\s*\{([^}]*)\}\s*\]', a[1])
h = V(m.group(1), 'u64')
c = [x.strip() for x in m.group(2).split(',')]
if 'base' in mods and mods >= {'2d', 'v4', 's32'} and mods & {'f32', 'f16'}:
E(f'{{ ::f4 t_ = tex_fetch({h}, (int32_t){V(c[0], "u32")}, (int32_t){V(c[1], "u32")});')
elif mods >= {'level', '2d', 'v4', 'f32'} and op.endswith('f32.f32'):
E(f'{{ ::f4 t_ = tex_level({h}, {V(c[0], "f32")}, {V(c[1], "f32")}, {V(a[2], "f32")});')
else:
raise Err(op)
for i, d in enumerate(dst):
A(d, f'(h1)t_[{i}]', 'f16') if 'f16' in mods else A(d, f't_[{i}]', 'float')
E('}')
return
if base == 'tld4':
if not (mods >= {'r', '2d', 'v4', 'f32'}):
raise Err(op)
dst = [x.strip() for x in a[0][1:-1].split(',')]
m = re.match(r'\[\s*(%rd\d+)\s*,\s*\{([^}]*)\}\s*\]', a[1])
c = [x.strip() for x in m.group(2).split(',')]
E(f'{{ ::f4 t_ = tex_gather_r({V(m.group(1), "u64")}, {V(c[0], "f32")}, {V(c[1], "f32")});')
for i, d in enumerate(dst):
A(d, f't_[{i}]', 'float')
E('}')
return
if base == 'sust':
m = re.match(r'\[\s*(%rd\d+)\s*,\s*\{([^}]*)\}\s*\]', a[0])
h = V(m.group(1), 'u64')
c = [V(x.strip(), 'u32') for x in m.group(2).split(',')]
v = [V(x.strip(), 'u32') for x in a[1][1:-1].split(',')]
if op == 'sust.p.2d.v4.b32.zero':
E(f'd4r_sust_p_v4b32({h}, (int32_t){c[0]}, (int32_t){c[1]}, {v[0]}, {v[1]}, {v[2]}, {v[3]});'); return
if op == 'sust.b.2d.b32.zero':
E(f'd4r_sust_b32({h}, (int32_t){c[0]}, (int32_t){c[1]}, {v[0]});'); return
if op == 'sust.b.2d.v2.b16.zero':
v = [V(x.strip(), 'u16') for x in a[1][1:-1].split(',')]
E(f'd4r_sust_v2b16({h}, (int32_t){c[0]}, (int32_t){c[1]}, {v[0]}, {v[1]});'); return
raise Err(op)
if base == 'shfl':
if op == 'shfl.sync.idx.b32':
d = a[0].split('|')
pred = f'&{self.reg(d[1])}' if len(d) > 1 else 'nullptr'
A(d[0], f'shfl_idx({V(a[1], "u32")}, {V(a[2], "u32")}, {V(a[3], "u32")}, {pred})', 'u32'); return
if op != 'shfl.sync.bfly.b32':
raise Err(op)
d = a[0].split('|')
p = f'&{self.reg(d[1])}' if len(d) > 1 else 'nullptr'
A(d[0], f'shfl_bfly({V(a[1], "u32")}, (int32_t){V(a[2], "u32")}, {V(a[3], "u32")}, {p})', 'u32'); return
if op == 'vote.sync.ballot.b32':
A(a[0], f'(uint32_t)(__builtin_amdgcn_ballot_w64({V(a[1], "pred")}) >> (wave_lane() & 32u))', 'u32'); return
raise Err('unhandled ' + op)
def cmp_op(self, mods):
table = {'lt': '{} < {}', 'gt': '{} > {}', 'le': '{} <= {}', 'ge': '{} >= {}', 'eq': '{} == {}',
'ne': '{} != {}',
# unordered: true if either is NaN
'ltu': '!({} >= {})', 'gtu': '!({} <= {})', 'leu': '!({} > {})', 'geu': '!({} < {})',
'equ': '!({} != {})', 'neu': '!({} == {})'}
for k in table:
if k in mods:
if k in ('ne', 'equ') and mods & {'f32', 'f16', 'f16x2', 'f64'}:
# PTX setp.ne.f32 is ordered (false on NaN) and equ is unordered (true on NaN), while C's
# != is unordered: spell both out. Integer ne is plain.
return '({0} < {1} || {0} > {1})' if k == 'ne' else '!({0} < {1} || {0} > {1})'
return table[k]
raise Err('compare ' + str(mods))
def cvt(self, op, o, a):
A, V = self.assign, self.val
mods = set(o)
if op == 'cvt.rn.satfinite.e4m3x2.f16x2':
A(a[0], f'e4m3x2({V(a[1], "u32")})', 'u16'); return
if op == 'cvt.rn.f16x2.e4m3x2':
A(a[0], f'd4r_decode_e4m3x2({V(a[1], "u16")})', 'u32'); return
types = [t for t in o if re.match(r'^(f|s|u|b)\d+(x2)?$', t)]
dt, stp = types[0], types[1]
if dt == 'f32' and stp == 'f32':
x = V(a[1], 'f32')
if 'sat' in mods and 'ftz' in mods and not (mods & {'rzi', 'rmi', 'rni', 'rpi'}):
A(a[0], f'sat_ftz({x})', 'float'); return
if mods & {'sat'}:
raise Err(op)
if 'rzi' in mods:
A(a[0], f'__builtin_truncf({x})', 'float'); return
if 'rmi' in mods:
A(a[0], f'__builtin_floorf({x})', 'float'); return
if 'rni' in mods:
A(a[0], f'__builtin_rintf({x})', 'float'); return
if 'rpi' in mods:
A(a[0], f'__builtin_ceilf({x})', 'float'); return
if 'ftz' in mods:
A(a[0], f'canon({x})', 'float'); return
A(a[0], x, 'float'); return
if dt == 's32' and stp == 'f32':
x = V(a[1], 'f32')
if 'rzi' in mods:
A(a[0], f'(uint32_t)f2i_rz({x})', 'u32'); return
if 'rmi' in mods:
A(a[0], f'(uint32_t)f2i_rz(__builtin_floorf({x}))', 'u32'); return
if 'rni' in mods:
A(a[0], f'(uint32_t)f2i_rz(__builtin_rintf({x}))', 'u32'); return
raise Err(op)
if dt == 'u32' and stp == 'f32' and 'rzi' in mods:
A(a[0], f'f2u_rz({V(a[1], "f32")})', 'u32'); return
if dt == 'f32' and stp == 's32':
A(a[0], f'(float)(int32_t){V(a[1], "u32")}', 'float'); return
if dt == 'f32' and stp == 'u32':
A(a[0], f'(float){V(a[1], "u32")}', 'float'); return
if dt == 'f32' and stp == 'f16':
A(a[0], f'(float){V(a[1], "f16")}', 'float'); return
if dt == 'f16' and stp == 'f32':
if 'rn' not in mods:
raise Err(op)
A(a[0], f'(h1){V(a[1], "f32")}', 'f16'); return
if dt == 'f16x2' and stp == 'f32':
# d = {cvt(a) in the high half, cvt(b) in the low half}
A(a[0], f'pack16(ashu((h1){V(a[2], "f32")}), ashu((h1){V(a[1], "f32")}))', 'u32'); return
if dt == 'f16' and stp == 'f16':
x = V(a[1], 'f16')
if 'rni' in mods:
A(a[0], f'h_roundeven({x})', 'f16'); return
if 'rmi' in mods:
A(a[0], f'h_floor({x})', 'f16'); return
raise Err(op)
if dt == 's32' and stp == 'f16':
x = V(a[1], 'f16')
if 'rni' in mods:
A(a[0], f'(uint32_t)h2i_sat(h_roundeven({x}))', 'u32'); return
if 'rmi' in mods:
A(a[0], f'(uint32_t)h2i_sat(h_floor({x}))', 'u32'); return
raise Err(op)
if dt == 'f16' and stp == 's32':
A(a[0], f'(h1)(int32_t){V(a[1], "u32")}', 'f16'); return
if dt == 's64' and stp == 's32':
A(a[0], f'(uint64_t)(int64_t)(int32_t){V(a[1], "u32")}', 'u64'); return
if dt == 'u64' and stp == 'u32':
A(a[0], f'(uint64_t){V(a[1], "u32")}', 'u64'); return
if dt in ('u16', 's16') and stp in ('u32', 's32'):
A(a[0], f'(uint16_t){V(a[1], "u32")}', 'u16'); return
if dt in ('u32', 's32') and stp in ('u16',):
A(a[0], f'(uint32_t){V(a[1], "u16")}', 'u32'); return
if dt in ('u32', 's32') and stp in ('s16',):
A(a[0], f'(uint32_t)(int32_t)(int16_t){V(a[1], "u16")}', 'u32'); return
if dt == 'u32' and stp == 'u64':
A(a[0], f'(uint32_t){V(a[1], "u64")}', 'u32'); return
raise Err('cvt ' + op)
def mem_type(self, o):
for t in ('u8', 's8', 'u16', 's16', 'b16', 'u32', 's32', 'b32', 'f32', 'u64', 's64', 'b64', 'f16'):
if t in o:
return t
raise Err('mem type ' + str(o))
def ctype_of(self, t):
return {'u8': 'uint8_t', 's8': 'int8_t', 'u16': 'uint16_t', 's16': 'int16_t', 'b16': 'uint16_t',
'u32': 'uint32_t', 's32': 'int32_t', 'b32': 'uint32_t', 'f32': 'float', 'u64': 'uint64_t',
's64': 'int64_t', 'b64': 'uint64_t'}[t]
def ptr(self, space, base, off, ct):
if base == 'KARG':
return f'*(const AS4 {ct}*)(KARGP + {off})'
if space == 'param':
return f'*(const AS4 {ct}*)(uintptr_t)({base} + {off})'
if space == 'shared':
return f'*(AS3 {ct}*)(uintptr_t)({base} + {off}u)'
if space == 'local':
return f'*(AS5 {ct}*)(uintptr_t)(uint32_t)({base} + {off})'
if space == 'global' and getattr(self, 'as1', False):
# --as1: ld/st.global through address-space-1 pointers (global_load / global_store instead of flat)
return f'*(AS1 {ct}*)(uintptr_t)({base} + {off})'
if space in ('global', 'local'):
return f'*({ct}*)(uintptr_t)({base} + {off})'
raise Err('space ' + space)
def ld(self, o, a):
space = next(s for s in ('param', 'global', 'shared', 'local') if s in o)
t = self.mem_type(o)
ct = self.ctype_of(t)
base, off = self.addr(a[1])
dsts = [x.strip() for x in a[0][1:-1].split(',')] if a[0].startswith('{') else [a[0]]
width = {'uint8_t': 1, 'int8_t': 1, 'uint16_t': 2, 'int16_t': 2, 'uint32_t': 4, 'int32_t': 4, 'float': 4,
'uint64_t': 8, 'int64_t': 8}[ct]
if len(dsts) in (2, 4) and width == 4 and space in ('global', 'shared') and base != 'KARG':
# ld.v2/v4 (naturally aligned in PTX) as one vector load instead of per-element loads
vt = 'u4' if len(dsts) == 4 else 'u2v'
asq = 'AS3 ' if space == 'shared' else ('AS1 ' if getattr(self, 'as1', False) else '')
ptr = f'(uintptr_t)({base} + {off}u)' if space == 'shared' else f'(uintptr_t)({base} + {off})'
self.emit(f'{{ const {vt} v_ = *({asq}const {vt}*){ptr};')
for i, d in enumerate(dsts):
self.assign(d, f'v_[{i}]', 'u32')
self.emit('}')
return
for i, d in enumerate(dsts):
e = self.ptr(space, base, off + i * width, ct)
if ct == 'float':
self.assign(d, e, 'float')
elif width == 8:
self.assign(d, e, 'u64')
else:
name = self.reg(d)
if self.rtype(name) == 'uint16_t':
self.assign(d, e, 'u16')
else:
self.assign(d, f'(uint32_t){e}' if ct != 'int8_t' and ct != 'int16_t' else f'(uint32_t)(int32_t){e}', 'u32')
def st(self, o, a):
space = next(s for s in ('param', 'global', 'shared', 'local') if s in o)
t = self.mem_type(o)
ct = self.ctype_of(t)
base, off = self.addr(a[0])
srcs = [x.strip() for x in a[1][1:-1].split(',')] if a[1].startswith('{') else [a[1]]
width = {'uint8_t': 1, 'uint16_t': 2, 'uint32_t': 4, 'int32_t': 4, 'float': 4, 'uint64_t': 8}[ct]
for i, s in enumerate(srcs):
v = self.val(s, 'f32' if ct == 'float' else 'u64' if width == 8 else 'u16' if width == 2 else 'u32')
self.emit(f'{self.ptr(space, base, off + i * width, ct)} = ({ct})({v});')
# ------------------------------------------------------------ roundf idiom
def rewrite_rz(self):
"""add.rz.ftz.f32 t, x, h; cvt.rzi.f32.f32 y, t (h = copysign(0.5, x)) -> y = roundf(x)"""
text = self.body
pat = re.compile(r'add\.rz\.ftz\.f32\s+(%f\d+),\s*(%f\d+),\s*(%f\d+);\s*(?://[^\n]*\s*)*cvt\.rzi\.f32\.f32\s+(%f\d+),\s*(%f\d+);')
count = 0
def repl(m):
nonlocal count
t, x, h, y, t2 = m.groups()
if t != t2:
raise Err('rz idiom mismatch')
uses = len(re.findall(re.escape(t) + r'\b', text))
if uses != 2:
raise Err(f'rz temp {t} used {uses} times')
# check h = copysign(0.5, x): mov.b32 rA, x; and.b32 rB, rA, -2147483648; or.b32 rC, rB, 1056964608; mov.b32 h, rC
count += 1
return f'd4r.roundf {y}, {x};'
self.body = pat.sub(repl, text)
return count
def main():
args = [a for a in sys.argv[1:] if not a.startswith('--')]
flags = {a for a in sys.argv[1:] if a.startswith('--')}
if len(args) != 2:
sys.exit(__doc__)
ptx, kernel = open(args[0]).read(), args[1]
mpart = None
if '--m-tail' in flags:
lines = ptx.replace('\r\n', '\n').split('\n')
if kernel == 'rrlite_enc0_4x4_mvhi_hdr_folded':
if lines[2547].strip() != 'bar.warp.sync -1;':
raise Err('enc0 cut changed')
lines = lines[:2548] + ['d4r.enc0 %r259, %rd106, %rd78, %r1066, %r1067, %r1230, %r1233;', 'ret;', '}']
mpart = 'enc0_tail'
elif kernel == 'rrlite_dec0_4x4_folded':
if lines[138].strip() != 'mov.u32 %r17, %laneid;' or lines[586].strip() != 'st.shared.v2.u16 [%r570+1168], {%rs63, %rs64};':
raise Err('dec0 cut changed')
lines[138:587] = ['mov.u32 %r560, _ZZ22rrlite_dec0_4x4_foldedN6RRLite18Decoder0ParametersEE4smem;',
'd4r.dec0 %r560, %rd22, %rd24, %r475, %r476, %r3, %r4;']
mpart = 'dec0_head'
else:
raise Err('--m-tail requires folded M enc0 or dec0')
ptx = '\n'.join(lines)
tr = Translator(ptx, kernel)
n_rz = tr.rewrite_rz()
if '--ifconv' in flags:
import ifconv, ctile as _ct
tr.body, n_if = ifconv.apply(tr.body, tokenize, _ct.parse, split_operands)
sys.stderr.write(f'ifconv: {n_if} diamonds converted\n')
stamps = [f.split('=', 1)[1].split(',') for f in flags if f.startswith('--stamps=')]
tr.stamps = stamps[0] if stamps else []
tr.bbcount = '--bbcount' in flags
tr.as1 = '--as1' in flags
tr.bblabels = []
ct = None
if '--ctile' in flags:
import ctile
ct = ctile.emit(tr, tokenize, Translator)
if '--sink' in flags:
import ifconv, ctile as _ct
tr.body, n_sink = ifconv.sink_stores(tr.body, tokenize, _ct.parse)
sys.stderr.write(f'sink: {n_sink} stores moved\n')
orig_op = tr.op
def op_with_round(op, a):
if op in ('d4r.hmin2', 'd4r.hmax2'):
fn = 'm_min2' if op == 'd4r.hmin2' else 'm_max2'
left = ', '.join(tr.val(v, 'f16') for v in a[2:4])
right = ', '.join(tr.val(v, 'f16') for v in a[4:6])
tr.emit(f'{{ uint32_t packed_ = ash2u({fn}((h2){{{left}}}, (h2){{{right}}}));')
tr.assign(a[0], 'packed_ & 0xffffu', 'u16')
tr.assign(a[1], 'packed_ >> 16', 'u16')
tr.emit('}')
return
if op in ('d4r.enc0', 'd4r.dec0'):
vals = [tr.val(v, t) for v, t in zip(a, ('u32', 'u64', 'u64', 'u32', 'u32', 's32', 's32'))]
tr.emit(f'd4r_{"enc0_tail" if op == "d4r.enc0" else "dec0_head"}({", ".join(vals)});')
return
if op == 'd4r.roundf':
tr.assign(a[0], f'__builtin_roundf({tr.val(a[1], "f32")})', 'float')
return
orig_op(op, a)
tr.op = op_with_round
tr.translate()
out = []
out.append(f'// Generated by kernels/native/ptx2hip.py from NVIDIA PTX ({kernel}); {n_rz} roundf idioms.')
out.append('#include "native_rt.h"')
if kernel.startswith('rrlite_'):
out.append('#include "m_ptx_rt.h"')
if 'cvt.rn.f16x2.e4m3x2' in ptx:
out.append('RT uint32_t d4r_decode_e4m3x2(uint16_t v) { h2 h; for (int i = 0; i < 2; ++i) { '
'uint32_t b = (v >> (8*i)) & 255u; '
'h[i] = (b & 127u) == 127u ? ash((uint16_t)(0x7fffu | ((b & 128u) << 8))) : '
'(h1)(ash((uint16_t)(((b & 127u) << 7) | ((b & 128u) << 8))) * (h1)256.0f); } return ash2u(h); }')
if mpart:
out.append(f'#include "../tex/{mpart}.hip"')
if ct is None:
for name, size, align in tr.shared:
out.append(f'__attribute__((shared)) __attribute__((aligned({max(align, 16)}))) static uint8_t {name}[{size}];')
else:
# one LDS block: the kernel's own arrays (path B) and the color tile (path A) are never live together
off = 0
for name, size, align in tr.shared:
off = (off + 15) & ~15
out.append(f'#define {name} (d4r_lds + {off})')
off += size
prelude, fill, info = ct
out.insert(2, f'__attribute__((shared)) __attribute__((aligned(16))) static uint8_t d4r_lds[{max(off, info["lds_bytes"])}];')
out.insert(2, f'// --ctile: {info["taps"]} color taps through an LDS tile, {len(info["groups"])} conversion shapes')
out.extend(prelude)
out.extend(fill)
out.append(f'struct __attribute__((aligned({tr.param_align}))) {kernel}_params {{ uint8_t b[{tr.param_size}]; }};')
maxntid = re.search(r'\.maxntid\s+(\d+)', tr.attrs)
lb = f'__attribute__((amdgpu_flat_work_group_size(1, {maxntid.group(1)})))' if maxntid else ''
bounds = [f.split('=', 1)[1] for f in flags if f.startswith('--max-block=')]
if bounds:
n = int(bounds[0])
if not 1 <= n <= 1024:
raise Err('invalid --max-block')
lb = f'__attribute__((amdgpu_flat_work_group_size(1, {n})))'
out.append(f'extern "C" __attribute__((global)) {lb} void {kernel}({kernel}_params)')
out.append('{')
out.append(' const AS4 uint8_t* KARGP = (const AS4 uint8_t*)__builtin_amdgcn_kernarg_segment_ptr();')
out.append(' const uint64_t KARGU = (uint64_t)(uintptr_t)KARGP;')
for name, size, align in tr.locals:
out.append(f' __attribute__((aligned({align}))) uint8_t {name}[{size}];')
if ct is not None:
out.append(' int32_t ct_x0 = 0, ct_y0 = 0, ct_w = 0, ct_h = 0;')
if tr.bbcount:
# diagnostic build: d4r_bbc[k] = number of waves that entered basic block k (label list in a comment)
out.insert(2, '__attribute__((device)) unsigned long long d4r_bbc[512];\n'
'#define D4R_BBC(k) do { if (wave_lane() == (uint32_t)__builtin_ctzll(__builtin_amdgcn_read_exec())) '
'__hip_atomic_fetch_add(&d4r_bbc[k], 1ull, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT); } while (0)\n'
'// d4r_bbc labels: ' + ' '.join(tr.bblabels))
if tr.stamps:
# diagnostic build: per-region wave cycles (20-bit SHADER_CYCLES deltas) summed into d4r_dbg[k],
# wave counts into d4r_dbg[32 + k]; region k ends at stamp label k (k = len + 1: kernel return)
out.insert(2, '__attribute__((device)) unsigned long long d4r_dbg[64];\n'
'#define D4R_STAMP(k) do { uint32_t n_ = (uint32_t)__builtin_readcyclecounter(); '
'if (wave_lane() == (uint32_t)__builtin_ctzll(__builtin_amdgcn_read_exec())) { '
'__hip_atomic_fetch_add(&d4r_dbg[k], (unsigned long long)((n_ - st_last) & 0xfffffu), __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT); '
'__hip_atomic_fetch_add(&d4r_dbg[32 + (k)], 1ull, __ATOMIC_RELAXED, __HIP_MEMORY_SCOPE_AGENT); } '
'st_last = n_; } while (0)')
out.append(' uint32_t st_last = (uint32_t)__builtin_readcyclecounter();')
by_type = {}
for name, t in sorted(tr.regs.items()):
by_type.setdefault(t, []).append(name)
for t, names in by_type.items():
for i in range(0, len(names), 16):
out.append(f' {t} {", ".join(names[i:i + 16])};')
out.extend(tr.lines)
out.append('}')
print('\n'.join(out))
if __name__ == '__main__':
main()
+166
View File
@@ -0,0 +1,166 @@
// Native runtime for PTX kernels translated to HIP by ptx2hip.py (no ZLUDA compile involved).
// Every helper reproduces ZLUDA's lowering of the PTX instruction (ptx/src/pass/llvm/emit.rs and
// ptx/lib/zluda_ptx_impl.cpp, patches/zluda), so a translated kernel computes the same values as
// ZLUDA's own compile of NVIDIA's PTX. Texture and surface objects are ZLUDA handles: the address of
// a HIP image descriptor (8 dwords), its sampler (4 dwords at +48 bytes) and the CUDA format (+80).
// Compile flags (see build_native.sh): -ffp-contract=off, f32 denormals flushed (ZLUDA's
// "denormal-fp-math-f32"="preserve-sign"), f16 denormals kept.
#pragma once
#include <stdint.h>
#pragma clang fp contract(off)
#define RT static __attribute__((device, always_inline)) inline
#define AS4 __attribute__((address_space(4)))
#define AS3 __attribute__((address_space(3)))
#define AS1 __attribute__((address_space(1)))
#define AS5 __attribute__((address_space(5)))
typedef _Float16 h1;
typedef _Float16 h2 __attribute__((ext_vector_type(2)));
typedef float f4 __attribute__((ext_vector_type(4)));
typedef float f2 __attribute__((ext_vector_type(2)));
typedef int i2 __attribute__((ext_vector_type(2)));
typedef int i8v __attribute__((ext_vector_type(8)));
typedef int i4v __attribute__((ext_vector_type(4)));
typedef uint32_t u4 __attribute__((ext_vector_type(4)));
typedef uint32_t u2v __attribute__((ext_vector_type(2)));
// ---------------------------------------------------------------- bit casts
RT float asf(uint32_t u) { return __builtin_bit_cast(float, u); }
RT uint32_t asu(float f) { return __builtin_bit_cast(uint32_t, f); }
RT h1 ash(uint16_t u) { return __builtin_bit_cast(h1, u); }
RT uint16_t ashu(h1 h) { return __builtin_bit_cast(uint16_t, h); }
RT h2 ash2(uint32_t u) { return __builtin_bit_cast(h2, u); }
RT uint32_t ash2u(h2 h) { return __builtin_bit_cast(uint32_t, h); }
RT uint32_t pack16(uint16_t lo, uint16_t hi) { return (uint32_t)lo | ((uint32_t)hi << 16); }
// ---------------------------------------------------------------- f32
RT float canon(float x) { return __builtin_canonicalizef(x); }
RT float fmaxn(float a, float b) { return __builtin_fmaximum_numf(a, b); }
RT float fminn(float a, float b) { return __builtin_fminimum_numf(a, b); }
RT float sat(float x) { return fminn(fmaxn(x, 0.0f), 1.0f); }
RT float sat_ftz(float x) { return sat(canon(x)); } // cvt.ftz.sat.f32.f32
RT float abs_ftz(float x) { return __builtin_fabsf(canon(x)); }
RT float neg_ftz(float x) { return -canon(x); }
RT float rcp_a(float x) { return __builtin_amdgcn_rcpf(x); } // rcp.approx(.ftz).f32
RT float div_a(float a, float b) { return a * __builtin_amdgcn_rcpf(b); } // div.approx.ftz.f32 (fdiv arcp afn)
RT float ex2_a(float x) { return __builtin_amdgcn_exp2f(x); }
RT float lg2_a(float x) { return __builtin_amdgcn_logf(x); }
RT float rsq_a(float x) { return __builtin_amdgcn_rsqf(x); }
// cvt.rzi.s32.f32: saturating, NaN -> 0 (llvm.fptosi.sat, v_cvt_i32_f32); spelled out, a plain C cast is UB out of range
// Branch-free: the operand is clamped into int range first (fmaxf(NaN, lo) = lo), so the cast is always defined and
// the compiler can use selects (the guarded form compiled to a branch per conversion, splitting basic blocks).
RT int32_t f2i_rz(float x)
{
#ifdef D4R_NATIVE_F2I
// AMD V_CVT_I32_F32 truncates, saturates out-of-range inputs and maps NaN to zero.
// Explicit instruction avoids redundant clamps/selects emitted for a C saturating cast.
int32_t r;
__asm__("v_cvt_i32_f32 %0, %1" : "=v"(r) : "v"(x));
return r;
#else
const float c = __builtin_fminf(__builtin_fmaxf(x, -2147483648.0f), 2147483520.0f);
int32_t r = (int32_t)c;
r = x >= 2147483648.0f ? 2147483647 : r;
return x != x ? 0 : r;
#endif
}
// cvt.rzi.u32.f32: ZLUDA emits a plain fptoui (frozen) and relies on V_CVT_U32_F32 (truncates; negative and NaN -> 0,
// too large -> 0xffffffff), so the native helper is that instruction
RT uint32_t f2u_rz(float x)
{
uint32_t r;
__asm__("v_cvt_u32_f32 %0, %1" : "=v"(r) : "v"(x));
return r;
}
// ---------------------------------------------------------------- f16 (denormals kept)
RT h1 hfma(h1 a, h1 b, h1 c) { return __builtin_fmaf16(a, b, c); }
RT h2 h2fma(h2 a, h2 b, h2 c) { return __builtin_elementwise_fma(a, b, c); }
RT h1 hmaxn(h1 a, h1 b) { return __builtin_fmaximum_numf16(a, b); }
RT h1 hminn(h1 a, h1 b) { return __builtin_fminimum_numf16(a, b); }
RT h1 hsat(h1 x) { return hminn(hmaxn(x, (h1)0.0f), (h1)1.0f); }
RT h2 h2sat(h2 x) { return (h2){hsat(x.x), hsat(x.y)}; }
RT h2 h2abs(h2 x) { return __builtin_elementwise_abs(x); }
// ex2.approx.f16x2: v_exp_f32 per half (zluda_ptx_impl.cpp)
RT h2 h2ex2(h2 a) { return (h2){(h1)__builtin_amdgcn_exp2f((float)a.x), (h1)__builtin_amdgcn_exp2f((float)a.y)}; }
// cvt.rni/rmi.s32.f16: llvm.fptosi.sat.i32.f16 after the rounding (|f16| < 2^31, NaN -> 0)
RT int32_t h2i_sat(h1 h)
{
#ifdef D4R_NATIVE_H2I
return f2i_rz((float)h);
#else
return h != h ? 0 : (int32_t)(float)h;
#endif
}
RT h1 h_roundeven(h1 h) { return __builtin_elementwise_roundeven(h); }
RT h1 h_floor(h1 h) { return __builtin_elementwise_floor(h); }
// ---------------------------------------------------------------- threads (wave64: two CUDA warps per wave)
RT uint32_t wave_lane() { return __builtin_amdgcn_mbcnt_hi(~0u, __builtin_amdgcn_mbcnt_lo(~0u, 0u)); }
RT uint32_t warp_base()
{
#if __AMDGCN_WAVEFRONT_SIZE == 64
return wave_lane() & 32u;
#else
return 0u;
#endif
}
RT uint32_t laneid() { return wave_lane() & 31u; }
// shfl.sync.bfly.b32 d|p, a, delta, opts, mask (ZLUDA's SHFL_SYNC_IMPL)
RT uint32_t shfl_bfly(uint32_t input, int32_t delta, uint32_t opts, bool* in_bounds)
{
int32_t section_mask = (opts >> 8) & 31, warp_end = opts & 31;
delta &= 31;
int32_t self = (int32_t)laneid();
int32_t subsection = section_mask & self;
int32_t subsection_end = subsection | (~section_mask & warp_end);
int32_t idx = self ^ delta;
bool oob = idx > subsection_end;
if (oob)
idx = self;
if (in_bounds)
*in_bounds = !oob;
return (uint32_t)__builtin_amdgcn_ds_bpermute((int32_t)(warp_base() | (uint32_t)idx) << 2, (int32_t)input);
}
RT uint32_t bfi32(uint32_t insert, uint32_t base, uint32_t pos_32, uint32_t len_32)
{
uint32_t pos = pos_32 & 0xffu, len = len_32 & 0xffu;
if (pos >= 32)
return base;
uint32_t mask = len >= 32 ? (0xffffffffu << pos) : (((len >= 32 ? 0u : (1u << len)) - 1u) << pos);
return (~mask & base) | (mask & (insert << pos));
}
// ---------------------------------------------------------------- textures (ZLUDA handles)
__attribute__((device)) static uint32_t d4r_null_texture[32];
RT uint64_t tex_or_null(uint64_t t) { return t != 0 ? t : (uint64_t)(uintptr_t)d4r_null_texture; }
RT i8v tdesc(uint64_t t) { return *(const AS4 i8v*)(uintptr_t)tex_or_null(t); }
RT i4v sdesc(uint64_t t) { return *(const AS4 i4v*)(uintptr_t)(tex_or_null(t) + 48); }
__attribute__((device)) f4 llvm_image_load_mip_2d(uint32_t, int32_t, int32_t, int32_t, i8v, int, int)
__asm("llvm.amdgcn.image.load.mip.2d.v4f32.i32");
__attribute__((device)) f4 llvm_image_sample_l_2d(uint32_t, float, float, float, i8v, i4v, bool, int, int)
__asm("llvm.amdgcn.image.sample.l.2d.v4f32.f32");
extern "C" __attribute__((device)) f4 __ockl_image_gather4r_2D(const AS4 uint32_t* image, const AS4 uint32_t* sampler, f2 coord);
// tex.base.2d.v4.f32.s32: texel load, mip 0
RT f4 tex_fetch(uint64_t t, int32_t x, int32_t y) { return llvm_image_load_mip_2d(0xf, x, y, 0, tdesc(t), 0, 0); }
// tex.level.2d.v4.f32.f32
RT f4 tex_level(uint64_t t, float x, float y, float lod) { return llvm_image_sample_l_2d(0xf, x, y, lod, tdesc(t), sdesc(t), false, 0, 0); }
// tld4.r.2d.v4.f32.f32
RT f4 tex_gather_r(uint64_t t, float x, float y)
{
const AS4 uint32_t* image = (const AS4 uint32_t*)(uintptr_t)tex_or_null(t);
return __ockl_image_gather4r_2D(image, image + 12, (f2){x, y});
}
// bar.sync 0 (HIP's __syncthreads)
RT void bar_sync()
{
__builtin_amdgcn_fence(__ATOMIC_RELEASE, "workgroup");
__builtin_amdgcn_s_barrier();
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE, "workgroup");
}
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Build the staged output rewrite from the exact translated kernel.
Replaces path B's cooperative tile fill with output_tile.h. All other phases and
the DLAA path remain intact. Refuses to generate if the region's register contract
changes, so new translator output cannot silently reuse an incompatible mapping.
"""
import argparse
import re
from pathlib import Path
REGISTER = re.compile(r"\b(?:rd|rs|r|f|fd|p)\d+\b")
ASSIGN = re.compile(r"^\s*((?:rd|rs|r|f|fd|p)\d+)\s*=")
LABEL = re.compile(r"^(L__BB0_\d+):;$")
def rewrite(source):
head, main = source.split('extern "C" __attribute__((global))', 1)
lines = main.splitlines(keepends=True)
labels = [(i, LABEL.match(line.strip()).group(1)) for i, line in enumerate(lines) if LABEL.match(line.strip())]
blocks = {name: (start, labels[k + 1][0] if k + 1 < len(labels) else len(lines))
for k, (start, name) in enumerate(labels)}
pending, region = ['L__BB0_124'], set()
while pending:
name = pending.pop()
if name in region or name == 'L__BB0_138':
continue
region.add(name)
start, end = blocks[name]
body = ''.join(lines[start + 1:end])
pending += re.findall(r'goto (L__BB0_\d+);', body)
if not re.search(r'^\s*goto L__BB0_\d+;\s*$', lines[end - 1]):
# This region's fallthroughs are only its internal branch joins.
pending += [n for i, n in labels if i == end]
expected = {'L__BB0_124'} | {f'L__BB0_{k}' for k in range(127, 138)}
if region != expected:
raise ValueError(f'tile CFG changed: {sorted(region)}')
removed = {i for name in region for i in range(*blocks[name])}
definitions = {m.group(1) for i in removed if (m := ASSIGN.match(lines[i]))}
# Definitions declared at function scope count as uses. Ignore declaration-only lines.
uses_outside = set(REGISTER.findall(''.join(line for i, line in enumerate(lines)
if i not in removed and not re.match(r'^\s*(?:uint\d+_t|float|double|bool)\s+(?:rd|rs|r|f|fd|p)\d+(?:,|;)', line))))
live_out = definitions & uses_outside
if live_out != {'r215', 'r217'}:
raise ValueError(f'tile register contract changed: {sorted(live_out)}')
replacement = '''L__BB0_124:;
output_tile_fill(KARGP, (int32_t)r3, (int32_t)r4, (int32_t)r7, (int32_t)(r8 + 1u),
f1, (AS3 uint8_t*)sharedColor, (AS3 uint8_t*)sharedDepth);
r215 = (uint32_t)__builtin_amdgcn_workitem_id_y();
r217 = (uint32_t)__builtin_amdgcn_workitem_id_x();
goto L__BB0_138;
'''
result = []
for i, line in enumerate(lines):
if i == blocks['L__BB0_124'][0]:
result.append(replacement)
if i not in removed:
result.append(line)
return head + '#include "output_tile.h"\n' + 'extern "C" __attribute__((global))' + ''.join(result)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('source', type=Path)
args = parser.parse_args()
print(rewrite(args.source.read_text()), end='')