feat(hw-oracle): make the group segment size an option

The template allocated a fixed 4 KiB of LDS, so kernels with immediate
offsets past it could not run. --lds (Python lds) sets the group segment
size in bytes, up to 64 KiB, with 4096 as the default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ale 2026-10-09 02:24:02 -03:00
parent d70b899894
commit 8d30c27589
4 changed files with 54 additions and 11 deletions

View File

@ -26,9 +26,12 @@ from the GPU (`HW_ORACLE_TARGET` overrides it).
- `v4`-`v7` hold the row's 4 input dwords. `v10`-`v25` start at 0 and are stored as the result (16 dwords).
- Leave `v0` (lane id), `v1` (input offset), `v2` (output offset) and `s[4:7]` (input and output addresses) unchanged.
- 4 KiB of LDS. Rows run in workgroups of up to 1024 lanes, padded to whole waves with zero rows.
- 4 KiB of LDS, or the group segment size `--lds` (Python `lds`) gives in bytes, up to 64 KiB. Rows run in workgroups of up to
1024 lanes, padded to whole waves with zero rows.
- Bytes passed as `extra` follow the rows of each dispatch, at `s[4:5] + 16 * rows`: buffer contents, texels, etc.
Build buffer and image descriptors in SGPRs from that address.
Build buffer and image descriptors in SGPRs from that address. A kernel that needs more than 4 input dwords per lane
takes the rest from there too, for example `v_lshlrev_b32 v40, 3, v0` / `v_add_nc_u32 v40, 16 * rows, v40` /
`global_load_dwordx2 v[8:9], v40, s[4:5]` for two more dwords per lane, with `rows` the padded count.
Every run must explicitly set all floating-point controls, including runs of integer instructions. The CLI flags
use hyphens; Python keywords use underscores. Record these settings alongside the GPU and measured results.

View File

@ -11,6 +11,8 @@ from pathlib import Path
HERE = Path(__file__).resolve().parent
CACHE = Path(os.environ.get("HW_ORACLE_CACHE") or Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "anyps5-hw-oracle")
ROWS_PER_DISPATCH = 1024
DEFAULT_LDS = 4096
MAX_LDS = 65536
def rocm_root():
@ -53,18 +55,20 @@ def target():
return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip()
def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow):
def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS):
modes = (("ieee", ieee, 2), ("denorm32", denorm32, 4), ("denorm16", denorm16, 4),
("dx10_clamp", dx10_clamp, 2), ("round32", round32, 4), ("round16", round16, 4),
("fp16_overflow", fp16_overflow, 2))
for name, value, limit in modes:
if not isinstance(value, int) or value not in range(limit):
raise ValueError(f"{name} must be an integer from 0 to {limit - 1}")
if not isinstance(lds, int) or lds not in range(MAX_LDS + 1):
raise ValueError(f"lds must be an integer from 0 to {MAX_LDS}")
text = (HERE / "template.s").read_text()
for key, value in (("@TARGET@", target()), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"),
("@DENORM@", str(int(denorm32))), ("@DENORM16@", str(int(denorm16))), ("@IEEE@", str(int(ieee))),
("@DX10_CLAMP@", str(int(dx10_clamp))), ("@ROUND32@", str(int(round32))), ("@ROUND16@", str(int(round16))),
("@FP16_OVERFLOW@", str(int(fp16_overflow))), ("@BODY@", body)):
("@FP16_OVERFLOW@", str(int(fp16_overflow))), ("@LDS@", str(lds)), ("@BODY@", body)):
text = text.replace(key, value)
(work / "k.s").write_text(text)
subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", *(["-mwavefrontsize64"] if wave64 else []), "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True)
@ -72,7 +76,7 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3
return work / "k.co"
def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow):
def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS):
rows = [tuple(r) for r in rows]
wave = 64 if wave64 else 32
padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave)
@ -84,7 +88,7 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)
code = assemble(body, work, wave64, ieee=ieee, denorm32=denorm32, denorm16=denorm16,
dx10_clamp=dx10_clamp, round32=round32, round16=round16, fp16_overflow=fp16_overflow)
dx10_clamp=dx10_clamp, round32=round32, round16=round16, fp16_overflow=fp16_overflow, lds=lds)
for start in range(0, len(padded), ROWS_PER_DISPATCH):
chunk = padded[start:start + ROWS_PER_DISPATCH]
(work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra)
@ -107,6 +111,7 @@ def main():
parser.add_argument("--round16", type=int, choices=range(4), required=True)
parser.add_argument("--fp16-overflow", type=int, choices=(0, 1), required=True)
parser.add_argument("--coarse", action="store_true", help="input and output in coarse-grained GPU memory")
parser.add_argument("--lds", type=int, default=DEFAULT_LDS, metavar="BYTES", help=f"group segment size, 0 to {MAX_LDS} (default {DEFAULT_LDS})")
parser.add_argument("--extra", type=Path, help="bytes appended after the rows")
parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)")
args = parser.parse_args()
@ -117,7 +122,7 @@ def main():
for result in run(args.body.read_text(), rows, extra, args.wave64, args.coarse,
ieee=args.ieee, denorm32=args.denorm32, denorm16=args.denorm16,
dx10_clamp=args.dx10_clamp, round32=args.round32, round16=args.round16,
fp16_overflow=args.fp16_overflow):
fp16_overflow=args.fp16_overflow, lds=args.lds):
print(" ".join(f"{v:08x}" for v in result[:args.outs]))

View File

@ -40,7 +40,7 @@ probe:
.amdhsa_kernel probe
.amdhsa_user_sgpr_kernarg_segment_ptr 1
.amdhsa_kernarg_size 16
.amdhsa_group_segment_fixed_size 4096
.amdhsa_group_segment_fixed_size @LDS@
.amdhsa_next_free_vgpr 128
.amdhsa_next_free_sgpr 48
.amdhsa_wavefront_size32 @WAVE32@
@ -60,7 +60,7 @@ amdhsa.kernels:
- .name: probe
.symbol: probe.kd
.kernarg_segment_size: 16
.group_segment_fixed_size: 4096
.group_segment_fixed_size: @LDS@
.private_segment_fixed_size: 0
.kernarg_segment_align: 8
.wavefront_size: @WAVESIZE@

View File

@ -112,14 +112,49 @@ class FloatModeTests(unittest.TestCase):
argv += [item for name, value in modes.items() for item in ["--" + name.replace("_", "-"), str(value)]]
with patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[]) as run:
hw_oracle.main()
run.assert_called_once_with("s_nop 0", [(1, 2, 3, 4)], b"", True, True, **modes)
run.assert_called_once_with("s_nop 0", [(1, 2, 3, 4)], b"", True, True, **modes, lds=4096)
def test_run_forwards_modes_to_assembler(self):
with patch.object(hw_oracle, "assemble") as assemble:
hw_oracle.run("s_nop 0", [], wave64=True, **MODES)
self.assertEqual(assemble.call_args.args[0], "s_nop 0")
self.assertTrue(assemble.call_args.args[2])
self.assertEqual(assemble.call_args.kwargs, MODES)
self.assertEqual(assemble.call_args.kwargs, MODES | {"lds": 4096})
class GroupSegmentTests(unittest.TestCase):
def test_cli_forwards_lds(self):
with tempfile.TemporaryDirectory() as tmp:
body = Path(tmp) / "body.s"
rows = Path(tmp) / "rows.txt"
body.write_text("s_nop 0")
rows.write_text("1 2 3 4\n")
argv = ["hw_oracle.py", str(body), str(rows), "--lds", "8192"]
argv += [item for name, value in MODES.items() for item in ["--" + name.replace("_", "-"), str(value)]]
with patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[]) as run:
hw_oracle.main()
self.assertEqual(run.call_args.kwargs["lds"], 8192)
def test_run_forwards_lds_to_assembler(self):
with patch.object(hw_oracle, "assemble") as assemble:
hw_oracle.run("s_nop 0", [], lds=65536, **MODES)
self.assertEqual(assemble.call_args.kwargs["lds"], 65536)
def test_invalid_lds_fails_before_tool_lookup(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(hw_oracle, "target") as target:
for value in (-1, 65537, 0.5, "4096"):
with self.subTest(value=value), self.assertRaisesRegex(ValueError, "lds"):
hw_oracle.assemble("s_nop 0", Path(tmp), False, lds=value, **MODES)
target.assert_not_called()
def test_template_takes_lds(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(hw_oracle, "target", return_value="gfx1036"), \
patch.object(hw_oracle.subprocess, "run"), patch.object(hw_oracle, "tool", return_value="clang"):
hw_oracle.assemble("s_nop 0", Path(tmp), False, lds=8192, **MODES)
text = (Path(tmp) / "k.s").read_text()
self.assertIn(".amdhsa_group_segment_fixed_size 8192", text)
self.assertIn(".group_segment_fixed_size: 8192", text)
self.assertNotIn("@LDS@", text)
class AssemblyTests(unittest.TestCase):