Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions projects/hipdnn/tools/IngestorGenerator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,40 @@ echo '*.kdp.json diff=kdp' >> "$(git rev-parse --git-path info/attributes)"
The splice tool appends rather than interleaves, so its output reads as pure insertion
under every algorithm.

### Shipping a trained model

`uhd_gen promote` copies a trained model into the bundle's `heuristics/` subtree and adds
its UHD id to the UED, per arch: `sort_kernel_catalog.<arch>` after `default`, or a new
role (`predict_engine`, `predict_applicable_kernels`) at the end of the document. The
generator writes the UED too, so record the same binding in the config, or the next
regeneration drops it (and the regeneration gate's byte compare fails):

1. Run `uhd_gen promote` on the bundle. It prints the arch and the model's UHD id.
2. Add them to `engine.heuristics`, in the order promote added them. For a registry-driven
engine put them in the manifest's `engine` block (`configs/<bundle>.engine.yaml`) **and**
the selection (`configs/<bundle>.yaml`): `registry_enumerator.py` copies the manifest's
`engine` into every config it writes, and regeneration gate g4 requires the selection's
engine to equal it, so a binding only in the selection fails g4 and the next
enumeration drops it.

```yaml
engine:
heuristics:
sort_kernel_catalog:
gfx942: [4b750e22-2656-46ce-89e7-be9ba588d732]
```

3. Regenerate. The UED comes out byte-identical to the promoted one; nothing else
changes, and the reader still resolves the native UHD from `default` only, so the
model UHD under `heuristics/` mints and reuses no id.
4. `dvc add heuristics` in the bundle directory, and commit the `.dvc` pointer.

Roles are the three promote accepts. Keys are one concrete arch (`gfx942`, not `gfx9` or
`gfx950-dcgpu`); `default` is allowed except under `sort_kernel_catalog`, whose `default`
is the generator's native UHD (so that role also needs `heuristic: native`). Values are
non-empty lists of distinct UUIDs. A slotted engine refuses the key: its rankers are
per slot. Without `engine.heuristics` every output is unchanged.

## Output

The descriptor directory mirrors the authored root, so the emitted tree copies across
Expand Down Expand Up @@ -877,6 +911,9 @@ engine:
# Must be a single path stem (^[A-Za-z0-9_][A-Za-z0-9_-]*$).
knobs: [block_size] # optional; must all be int-typed kmd_fields
heuristic: native | none # optional, default "native"; "none" omits the UHD
heuristics: # optional; per-arch model UHDs `uhd_gen promote` bound,
sort_kernel_catalog: # role -> arch -> [UHD id]. See "Shipping a trained
gfx942: [<uhd-uuid>] # model". Not allowed beside `slots`.

kmd_fields: # the KMD's fields[] -- one per axis this engine's
- name: block_size # kernels vary along
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def load_config(path: Path) -> IngestorConfig:
census_name_symbol=engine_raw.get("census_name_symbol"),
shared_workspace=engine_raw.get("shared_workspace", ""),
slug_override=engine_raw.get("slug"),
heuristics=_load_heuristics(engine_raw, slotted),
)

kmd_fields = _read_kmd_fields(raw.get("kmd_fields", []), "kmd_fields")
Expand Down Expand Up @@ -735,6 +736,83 @@ def _require_sequence(value, scope: str, what: str = "field names") -> list:
return list(value)


#: The UED roles `uhd_gen promote --role` binds per arch, in the order it accepts them.
HEURISTIC_ROLES = (
"sort_kernel_catalog",
"predict_engine",
"predict_applicable_kernels",
)

#: One concrete architecture name. Mirrors `device_probe.ARCH_SHAPE` and its family-word
#: refusal (tools/device_probe.py), which `codegen` cannot import; a test pins the two.
_ARCH_TOKEN = re.compile(r"gfx[0-9a-f]{3,}(?:-[a-z]+)*")
_ARCH_FAMILY_WORD = re.compile(r"-(?:generic|all|dcgpu|dgpu|igpu)(?:-|$)")
_UUID = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")


def _is_arch_token(text) -> bool:
return (
isinstance(text, str)
and _ARCH_TOKEN.fullmatch(text) is not None
and not _ARCH_FAMILY_WORD.search(text)
)


def _load_heuristics(engine_raw: dict, slotted: bool) -> dict:
"""Read ``engine.heuristics``: role -> arch -> UHD ids, as `uhd_gen promote`
writes them into the UED. Absent is ``{}``, which emits nothing."""
raw = engine_raw.get("heuristics")
if raw is None:
return {}
scope = "engine.heuristics"
if slotted:
raise ConfigError(
f"{scope} is set on a slotted engine; a slotted engine binds its rankers "
f"per slot (slots[].ranker), so per-arch engine bindings are not supported."
)
_require_mapping(raw, scope)
heuristics = {}
for role, arches in raw.items():
if role not in HEURISTIC_ROLES:
raise ConfigError(
f"{scope} names role {role!r}; expected one of {list(HEURISTIC_ROLES)} "
f"(the roles `uhd_gen promote --role` accepts)."
)
_require_mapping(arches, f"{scope}.{role}")
if not arches:
raise ConfigError(f"{scope}.{role} is empty; bind at least one arch.")
if role == "sort_kernel_catalog" and engine_raw.get("heuristic") == "none":
raise ConfigError(
f"{scope}.sort_kernel_catalog is set but engine.heuristic is 'none'; "
f"the arch entries follow the native 'default' UHD, which 'none' omits."
)
bound = {}
for arch, ids in arches.items():
key = f"{scope}.{role}.{arch}"
if arch == "default":
if role == "sort_kernel_catalog":
raise ConfigError(
f"{key}: the generator owns sort_kernel_catalog.default (the "
f"native UHD from engine.heuristic); bind arch keys only."
)
elif not _is_arch_token(arch):
raise ConfigError(
f"{scope}.{role} key {arch!r} is not one concrete gfx architecture "
f"name (e.g. gfx942) or 'default'."
)
ids = _require_sequence(ids, key, "UHD ids")
if not ids:
raise ConfigError(f"{key} is an empty list; bind at least one UHD id.")
for uhd in ids:
if not isinstance(uhd, str) or not _UUID.fullmatch(uhd):
raise ConfigError(f"{key} holds {uhd!r}, which is not a UUID.")
if len(set(ids)) != len(ids):
raise ConfigError(f"{key} repeats a UHD id: {ids!r}.")
bound[arch] = ids
heuristics[role] = bound
return heuristics


def _drop_tag_slot(match) -> str:
"""Replace a `{tag}` slot and ONE adjacent separator when the tag is empty.

Expand Down Expand Up @@ -1337,6 +1415,7 @@ def _require_config_shapes(raw: dict) -> None:
"census_name_symbol",
"shared_workspace",
"slug",
"heuristics",
}
)
#: RFC 0021 §4's per-slot selection members, plus the slot's own ``specialization``.
Expand Down
11 changes: 10 additions & 1 deletion projects/hipdnn/tools/IngestorGenerator/codegen/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,13 +458,22 @@ def build_ued(config: IngestorConfig, ids: dict) -> dict:
"graph_match": {"native": config.graph_match_symbol},
"metadata": ids["kmd"],
}
bindings = config.engine.heuristics
if config.engine.has_heuristic:
# RFC 0019 §3.1: the loader rejects a bare top-level `heuristic` pointer.
ued["sort_kernel_catalog"] = {"default": ids["uhd"]}
# Arch entries follow `default`, where `uhd_gen promote` inserts them.
ued["sort_kernel_catalog"] = {
"default": ids["uhd"],
**bindings.get("sort_kernel_catalog", {}),
}
if config.engine.knobs:
ued["knobs"] = list(config.engine.knobs)
if config.engine.behavior_notes:
ued["behavior_notes"] = list(config.engine.behavior_notes)
# `promote` appends a new role at the end of the document.
for role, arches in bindings.items():
if role != "sort_kernel_catalog":
ued[role] = dict(arches)
return ued


Expand Down
3 changes: 3 additions & 0 deletions projects/hipdnn/tools/IngestorGenerator/codegen/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ class EngineSpec:
#: slug splits before every capital, so a local name ending in a digit-capital pair
#: (``Unified2D``) would come out as ``unified2_d``.
slug_override: str | None = None
#: Per-arch heuristic bindings `uhd_gen promote` writes into the UED: role ->
#: arch (or `default`) -> UHD ids, in config order. Empty emits nothing.
heuristics: dict[str, dict[str, list[str]]] = field(default_factory=dict)

@property
def namespace(self) -> str:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0",
"id": "4208dcf6-3128-4146-ab2a-affa5b6ab4eb",
"name": "hipkernel:Gfx942AttentionUnified3d",
"sdk_version": "1.0.0",
"graph_match": {
"native": "hipkernel.gfx942_attention_unified3d.graph_match"
},
"metadata": "fa42fab2-67a1-4154-b45f-55b25c1f5265",
"sort_kernel_catalog": {
"default": "c3569304-2234-4570-b3ab-d3e007ebe52a",
"gfx942": [
"4b750e22-2656-46ce-89e7-be9ba588d732"
]
},
"knobs": [
"block_size",
"num_segments"
]
}
127 changes: 127 additions & 0 deletions projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3050,3 +3050,130 @@ def test_a_noise_policy_key_must_be_a_finite_non_negative_number(
):
with pytest.raises(ConfigError, match=key):
self._load(tmp_path, {**self.REDUCTION, key: value})


class TestEngineHeuristics:
"""``engine.heuristics``: the per-arch UHD bindings `uhd_gen promote` writes."""

MODEL = "4b750e22-2656-46ce-89e7-be9ba588d732"
OTHER = "73276c7b-f432-437b-9a06-8bb62920c823"

@staticmethod
def _load(tmp_path, configs_dir, heuristics, base="scale_add.yaml", **engine):
raw = yaml.safe_load((configs_dir / base).read_text())
raw["engine"].update(engine, heuristics=heuristics)
path = tmp_path / base
path.write_text(yaml.safe_dump(raw, sort_keys=False))
return load_config(path)

def test_bindings_load_in_config_order(self, tmp_path, configs_dir):
config = self._load(
tmp_path,
configs_dir,
{
"predict_engine": {"gfx950": [self.MODEL], "default": [self.OTHER]},
"sort_kernel_catalog": {"gfx950": [self.OTHER], "gfx942": [self.MODEL]},
},
)
bindings = config.engine.heuristics
assert list(bindings) == ["predict_engine", "sort_kernel_catalog"]
assert list(bindings["sort_kernel_catalog"]) == ["gfx950", "gfx942"]
assert bindings["predict_engine"]["default"] == [self.OTHER]

def test_absent_is_empty(self, scale_add_config):
assert scale_add_config.engine.heuristics == {}

@pytest.mark.parametrize(
"heuristics, match",
[
({"rank_engine": {"gfx942": [MODEL]}}, "role 'rank_engine'"),
(
{"sort_kernel_catalog": {"gfx9": [MODEL]}},
"sort_kernel_catalog key 'gfx9'",
),
(
{"sort_kernel_catalog": {"gfx950-dcgpu": [MODEL]}},
"key 'gfx950-dcgpu'",
),
(
{"sort_kernel_catalog": {"default": [MODEL]}},
r"sort_kernel_catalog\.default",
),
({"sort_kernel_catalog": {"gfx942": []}}, r"sort_kernel_catalog\.gfx942"),
({"predict_engine": {"gfx942": ["not-a-uuid"]}}, r"predict_engine\.gfx942"),
({"predict_engine": {"gfx942": MODEL}}, r"predict_engine\.gfx942"),
(
{"predict_engine": {"gfx942": [MODEL, MODEL]}},
r"predict_engine\.gfx942 repeats",
),
({"predict_engine": {}}, r"heuristics\.predict_engine is empty"),
({"predict_engine": [MODEL]}, "predict_engine"),
([MODEL], "engine.heuristics"),
],
)
def test_refusals_name_the_key(self, tmp_path, configs_dir, heuristics, match):
with pytest.raises(ConfigError, match=match):
self._load(tmp_path, configs_dir, heuristics)

def test_arch_entries_need_the_native_default(self, tmp_path, configs_dir):
with pytest.raises(ConfigError, match="engine.heuristic is 'none'"):
self._load(
tmp_path,
configs_dir,
{"sort_kernel_catalog": {"gfx942": [self.MODEL]}},
heuristic="none",
)

def test_refused_beside_slots(self, tmp_path):
fixture = Path(__file__).parent / "fixtures" / "slotted_three_slot.yaml"
raw = yaml.safe_load(fixture.read_text())
raw["engine"]["heuristics"] = {"predict_engine": {"gfx942": [self.MODEL]}}
path = tmp_path / fixture.name
path.write_text(yaml.safe_dump(raw, sort_keys=False))
with pytest.raises(ConfigError, match="engine.heuristics is set on a slotted"):
load_config(path)

def test_refusal_exits_2(self, tmp_path, configs_dir, monkeypatch, capsys):
import generate

raw = yaml.safe_load((configs_dir / "scale_add.yaml").read_text())
raw["engine"]["heuristics"] = {"sort_kernel_catalog": {"gfx9": [self.MODEL]}}
path = tmp_path / "scale_add.yaml"
path.write_text(yaml.safe_dump(raw, sort_keys=False))
argv = [
"generate.py",
"--config",
str(path),
"--output-dir",
str(tmp_path / "o"),
]
monkeypatch.setattr(sys, "argv", argv)
with pytest.raises(SystemExit) as exit_info:
generate.main()
assert exit_info.value.code == 2
assert "engine.heuristics.sort_kernel_catalog" in capsys.readouterr().err

@pytest.mark.parametrize(
"token",
[
"gfx942",
"gfx90a",
"gfx1151",
"gfx1250-strict",
"gfx9",
"gfx11-generic",
"gfx950-dcgpu",
"gfx950-all",
"GFX942",
"default",
"sm90",
],
)
def test_arch_check_matches_device_probe(self, token):
tools = str(Path(__file__).parent.parent / "tools")
if tools not in sys.path:
sys.path.insert(0, tools)
import device_probe
from codegen.config_loader import _is_arch_token

assert _is_arch_token(token) == device_probe.is_arch_token(token)
Loading
Loading