diff --git a/dnn-providers/hip-kernel-provider/CMakeLists.txt b/dnn-providers/hip-kernel-provider/CMakeLists.txt index 6e9664f927b9..19d7f3bfc916 100644 --- a/dnn-providers/hip-kernel-provider/CMakeLists.txt +++ b/dnn-providers/hip-kernel-provider/CMakeLists.txt @@ -249,6 +249,8 @@ if(NOT TARGET hipdnn_flatbuffers_sdk) find_package(hipdnn_flatbuffers_sdk CONFIG REQUIRED) endif() if(NOT TARGET hipdnn_plugin_sdk) + # Drop a stale cached table path so an older SDK Config cannot be answered by it. + unset(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON CACHE) find_package(hipdnn_plugin_sdk CONFIG REQUIRED) endif() diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md index 4d7821d9948a..e75ec36c44be 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/README.md @@ -4,6 +4,42 @@ Build-time UKD/KMD/KDP -> kpack packaging. Provider-internal (`tools/hkp_pack.py see `python/hkp_pack/` for the pipeline and `examples/descriptors/` for a real, minimal authored source root. +## Generic GPU targets + +`arch` accepts the LLVM generic targets in `projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json` +(for example `gfx11-generic`), read through the required `--generic-targets-json`. Keep +`GPU_TARGETS` concrete. The packer builds each selected generic once and copies its descriptors +and archive (`arch: []`) into each selected member's folder; no generic-named folder is +installed. Disk use grows with the number of members. + +Copies depend only on the source tree, the generic's spelling, hipcc and the table, so they are +identical across member folders and separate per-arch builds, and the loader collapses them to +one entry. Copies from different compilers differ in `provenance`, and the loader drops both. + +Kernels sharing a metadata tuple are ranked per device by arch tier; see +[RFC 0017](../../../projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md#4-resolve-the-kernel-packs-that-name-this-engine-and-apply-the-arch-gate). + +Authoring rules. Each violation fails the packer, raises a generator ConfigError, and makes the +loader log an ERROR and drop the pack: + +- Every `-generic` name is in the table. +- One `arch` list never holds a generic and one of its members, or two generics sharing a member. +- A UKD names no generic its KDP does not list. +- Under a KDP listing generics, a UKD with its own `arch` lists every KDP generic and only + entries the KDP lists; an inline UKD without `arch` inherits the pack. + +Packer only: a standalone UKD with empty `arch` needs an empty-`arch` KDP; shards with intersecting member sets may not write one path with different bytes. +Packer and generator only: `kind: rocke` takes no generic. + +### hsaco kernels and `arch` + +An `hsaco` kernel's `arch` resolves in order: the kernel's own `arch`, else its pack's. The +result must be non-empty; the generator rejects the kernel otherwise, because an unrestricted +object would ship the same bytes to every shard. The generator writes the resolved list onto the +descriptor, and the packer requires a non-empty `arch` there; it does not inherit, so an inline +hsaco UKD without `arch` is refused even under an arch-scoped KDP. A generic-target object +lists each member or its generic. + ## Source roots and what the walk accepts Each wired root is walked recursively and packs into **its own** `OUT_ROOT`; no two @@ -137,10 +173,9 @@ bytes are packed as-is into the arch's kpack, the kernel signature is read from object's AMDGPU metadata as for a compiled object, and the UKD ships as `kind: kpack`. The toc key derives from the file's resolved root-relative path, so one file serving several symbols is one archive entry, and one key claimed by two different files is a -hard error. The packer does not check the object's format or target processor. An -`hsaco` UKD must list the arch(es) its object runs on in `arch` (a generic-target -object lists every arch it runs on); an absent or empty `arch` is rejected. The author's own load test on the target arch -is the only check; no in-tree load test covers `hsaco`. The shipped provenance records +hard error. The packer does not check the object's format or target processor; +`arch` follows [hsaco kernels and `arch`](#hsaco-kernels-and-arch). The author's own load +test on the target arch is the only check; no in-tree load test covers `hsaco`. The shipped provenance records `origin_kind: "hsaco"`, the root-relative `file`, its `sha256` and the `symbol`, and makes no toolchain claim. As for `hip`, the specialization contract must declare `metadata_fields: []`: no compiler ran whose specialization a binding could observe. @@ -406,8 +441,8 @@ Every architecture named explicitly under the production root gets an **automati in every lane that does not build it. At configure, `hkp_load_packaging_probes()` runs `tools/hkp_probe_derive_root.py --list-arches` on `HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT`: the union of the explicit `arch` -entries of every KDP and UKD (inline and standalone), read with the packer's own loader. -It subtracts the lane's own architectures (`hkp_selected_arches()`, i.e. `GPU_TARGETS`) +entries of every KDP and UKD (inline and standalone), read with the packer's own loader and +the generic target table, generic names mapped as below. It subtracts the lane's own architectures (`hkp_selected_arches()`, i.e. `GPU_TARGETS`) and declares `hkp_add_packaging_probe(ARCH )` for each one left: probe ``, ctest entry `hkp-probe-`, packing the default sample. Configure prints the result: @@ -428,13 +463,35 @@ build targets, so each lane's own production pack packs it, and its GPU test lan on every PR. Only an architecture the lane does not build needs a probe, and only an explicit `arch` entry can make a descriptor ship there and nowhere else. -**Non-concrete names are skipped.** An explicit `arch` entry that is not a concrete -target, such as a generic target (`gfx9-4-generic`) or a TheRock family name, gets no -automatic probe; configure prints which ones it skipped. No build packs for such a name: -`GPU_TARGETS` drops it, and the packer matches arch names exactly, so it never selects a -family member in its place either. Concrete names with a distinct-target word -(`gfx1250-strict`) are probed like any other arch. The rule is `hkp_is_concrete_arch()` -in `cmake/HkpPackaging.cmake`, the same one `GPU_TARGETS` goes through. +**Generic targets are probed through one member.** A generic target in the generic target +table (`projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json`) that a descriptor names +gets its probe through the first member the table lists for it: `gfx11-generic` through +`gfx1100`, `gfx12-generic` through `gfx1200`. The packer compiles the generic once, for the +generic target, and merges the result into each requested member's folder, so which member +carries the probe only names the folder; the table's order picks it, with no configuration. +The probe for `gfx1100` packs every pass that applies to `gfx1100`, its own and the +generic's: a KDP that ships only for the generic is trimmed like any other, and the +assertion expects its UKDs with `arch: gfx11-generic` and the generic's archive +(`kpack/hip_kernel_provider_gfx11-generic.kpack`) in the `gfx1100` folder. + +- A lane that targets any member of the generic gets no probe for it: its production pack + compiles the generic already. Linux (`gfx942`) probes `gfx11-generic` through `gfx1100`; + Windows (`gfx1151`, a member) does not. +- One probe per member arch: a member that is also named explicitly, or chosen for two + generics, is probed once, because `--arches ` runs every pass that applies to it. + +``` +hkp: generic targets probed through their first member: [gfx11-generic -> gfx1100] +hkp: automatic probe arches: [gfx1100, gfx950] (probe arches of the explicit arches under : [gfx1100, gfx950]; build targets: [gfx942]) +``` + +**TheRock family names are skipped; an unlisted generic fails configure.** An explicit `arch` +entry that is a TheRock family name gets no automatic probe; configure prints which ones it +skipped. No build packs for such a name: `GPU_TARGETS` drops it. A `*-generic` entry the table +does not list fails configure with the packer loader's message, since the packer rejects it +too. Concrete names with a distinct-target word (`gfx1250-strict`) are +probed like any other arch. The rule is `hkp_is_concrete_arch()` in +`cmake/HkpPackaging.cmake`, the same one `GPU_TARGETS` goes through. ### How to use @@ -737,7 +794,7 @@ test module imports from another. Helpers both sides use live in plain modules: ### Desk-check a variant set (`hkp_pack.desk_check`, `tools/hkp_desk_check.py`) ``` -tools/hkp_desk_check.py --mode {full,structural} +tools/hkp_desk_check.py --mode {full,structural} --generic-targets-json J [--provenance-root P [--descriptor-root R]] [--kpack-python-dir D] [--field F] [--drift-field F] diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake index c4ccb2ff78e5..af9b697c1068 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackaging.cmake @@ -491,12 +491,13 @@ function(hkp_wire_pack_target) --inter-root "${_inter_root}" --kpack-python-dir "${ARG_ROCM_KPACK_DIR}" --source-label "${ARG_NAME}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" ${_producer_arg} ${_exclude_args} COMMAND "${CMAKE_COMMAND}" -E make_directory "${ARG_OUT_ROOT}" COMMAND "${CMAKE_COMMAND}" -E touch "${_stamp}" DEPENDS "${HKP_TOOL}" ${_source_inputs} ${_tool_sources} - "${_input_manifest}" + "${_input_manifest}" "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" ${_producer_deps} COMMENT "hkp: packing root '${ARG_NAME}' for ${ARG_ARCHES}" VERBATIM) @@ -1238,11 +1239,13 @@ function(_hkp_root_probe out_shipped out_offered out_ok root arches exclude_fold set(_probe_py "import json, sys from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError +from hkp_pack.generic_targets import GenericTargets from hkp_pack.pipeline import offered_engines, shipped_engines lists = {k: tuple(v for v in rest.split(',') if v) - for k, _, rest in (a.partition('=') for a in sys.argv[2:])} + for k, _, rest in (a.partition('=') for a in sys.argv[3:])} try: - flat = load_flat_input(sys.argv[1], log=lambda *_args: None, + flat = load_flat_input(sys.argv[1], GenericTargets.load(sys.argv[2]), + log=lambda *_args: None, exclude_folders=lists['folders'], disabled_kinds=lists['kinds']) print(json.dumps({'shipped': shipped_engines(flat, lists['arches']), @@ -1253,6 +1256,7 @@ except HkpPackError as exc: execute_process( COMMAND "${CMAKE_COMMAND}" -E env "PYTHONPATH=${HKP_PYTHON_ROOT}" -- "${Python3_EXECUTABLE}" -c "${_probe_py}" "${root}" + "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" "arches=${_arch_csv}" "folders=${_folder_csv}" "kinds=${_kind_csv}" RESULT_VARIABLE _rc OUTPUT_VARIABLE _out @@ -1649,6 +1653,15 @@ endfunction() # fatal. # --------------------------------------------------------------------------- function(hkp_add_packaging) + if(NOT HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON + OR NOT EXISTS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") + message(FATAL_ERROR + "hkp: HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON names no file " + "('${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}'); build against a " + "hipdnn_plugin_sdk that ships gpu_generic_targets.json.") + endif() + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + CMAKE_CONFIGURE_DEPENDS "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") find_package(Python3 COMPONENTS Interpreter REQUIRED) hkp_resolve_kpack(_rocm_kpack_dir "${Python3_EXECUTABLE}") diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake index 68472cba0146..38504ee76167 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/cmake/HkpPackagingProbes.cmake @@ -39,8 +39,9 @@ endfunction() # cannot be derived, e.g. a kept KDP references a standalone UKD, or a listed UKD is # kept nowhere) and re-run configure when a source file or the derive tooling # changes. Each is passed as one --ukd; with none, derive selects per compile -# group (one UKD, or a value sweep for kinds with a sweep field). The tool writes /../expect.json, the list of kept UKDs the -# assertion expects; receives its path. FATAL when that list is empty: no +# group (one UKD, or a value sweep for kinds with a sweep field). The tool writes +# /../expect.json, the list of kept UKDs the assertion expects, each with the +# pass it packs under (, or a generic target containing it); receives its path. FATAL when that list is empty: no # KDP under ships for , so the pack would prune everything. Deriving # is idempotent, so a reconfigure leaves the derived root's mtimes alone and does not # re-pack. @@ -50,7 +51,8 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_derive_inputs} "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" - "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py" + "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") get_property(_filter_args GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS) set(_ukd_args "") foreach(_ukd IN LISTS ARGN) @@ -60,6 +62,7 @@ function(_hkp_probe_derive_root name from_dir arch out_dir expect_var) COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" --from "${from_dir}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" --arch "${arch}" --out "${out_dir}" ${_filter_args} @@ -290,11 +293,16 @@ endfunction() # UKD under HIPKERNELPROVIDER_PRODUCTION_DESCRIPTOR_SOURCE_ROOT that is a concrete # target (hkp_is_concrete_arch), minus the build's own targets (hkp_selected_arches). # Descriptors with no `arch` (wildcards) name no arch: they ship for every build -# target, so the build's own production pack covers them. A non-concrete entry -# (gfx9-4-generic) is skipped with a STATUS line: no build packs for it, because -# GPU_TARGETS drops it and the packer matches arch names exactly. -# FATAL with the tool's stderr when the packer's loader rejects the root. Configure -# re-runs when a file under the root or the derive tooling changes. +# target, so the build's own production pack covers them. A generic target in +# HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON is probed through its first member (the +# packer compiles the generic for any member and merges it into the member's folder), +# unless the build targets one of its members, whose production pack compiles it +# already; the STATUS line shows each generic -> member mapping. A TheRock family name +# is skipped with a STATUS line: no build packs for it, because GPU_TARGETS drops it. +# FATAL with the tool's stderr when the packer's loader rejects the root, including a +# `*-generic` entry absent from the table. Configure +# re-runs when a file under the root, the generic target table or the derive tooling +# changes. # --------------------------------------------------------------------------- function(_hkp_probe_automatic_arches out_var) get_property(_filter_args GLOBAL PROPERTY HKP_PROBE_FILTER_ARGS) @@ -303,11 +311,20 @@ function(_hkp_probe_automatic_arches out_var) set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_root_inputs} "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" - "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py") + "${HKP_PKG_DIR}/tools/hkp_probe_kinds.py" + "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}") + hkp_selected_arches(_targets _targets_source) + set(_exclude_args "") + foreach(_target IN LISTS _targets) + list(APPEND _exclude_args --exclude "${_target}") + endforeach() execute_process( COMMAND "${Python3_EXECUTABLE}" "${HKP_PKG_DIR}/tools/hkp_probe_derive_root.py" - --list-arches --from "${_root}" ${_filter_args} + --list-arches --from "${_root}" + --generic-targets-json "${HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON}" + ${_exclude_args} + ${_filter_args} RESULT_VARIABLE _rc OUTPUT_VARIABLE _out ERROR_VARIABLE _err) @@ -317,19 +334,32 @@ function(_hkp_probe_automatic_arches out_var) "failed (exit ${_rc}).\n${_err}") endif() string(REGEX REPLACE "\n+$" "" _out "${_out}") - string(REPLACE "\n" ";" _named "${_out}") + string(REPLACE "\n" ";" _lines "${_out}") - hkp_selected_arches(_targets _targets_source) + # Each line is an arch followed by the generic targets it stands for. + set(_named "") set(_auto "") set(_skipped "") - foreach(_arch IN LISTS _named) + set(_mapped "") + foreach(_line IN LISTS _lines) + separate_arguments(_words UNIX_COMMAND "${_line}") + list(POP_FRONT _words _arch) + list(APPEND _named "${_arch}") hkp_is_concrete_arch(_concrete "${_arch}") if(_concrete) list(APPEND _auto "${_arch}") + foreach(_generic IN LISTS _words) + list(APPEND _mapped "${_generic} -> ${_arch}") + endforeach() else() list(APPEND _skipped "${_arch}") endif() endforeach() + if(_mapped) + list(JOIN _mapped ", " _mapped_text) + message(STATUS + "hkp: generic targets probed through their first member: [${_mapped_text}]") + endif() if(_skipped) list(JOIN _skipped ", " _skipped_text) message(STATUS @@ -343,8 +373,8 @@ function(_hkp_probe_automatic_arches out_var) list(JOIN _named ", " _named_text) list(JOIN _targets ", " _targets_text) message(STATUS - "hkp: automatic probe arches: [${_auto_text}] (explicit arches under ${_root}: " - "[${_named_text}]; build targets: [${_targets_text}])") + "hkp: automatic probe arches: [${_auto_text}] (probe arches of the explicit " + "arches under ${_root}: [${_named_text}]; build targets: [${_targets_text}])") set(${out_var} "${_auto}" PARENT_SCOPE) endfunction() @@ -375,8 +405,9 @@ endfunction() # probed as usual; an arch left with no content names no probe. Load the named # probes from probes/probes.cmake, then declare one automatic probe per architecture the production # root names that the build does not target: the explicit `arch` entries of its KDPs -# and UKDs (hkp_probe_derive_root.py --list-arches; wildcard descriptors name none) -# minus hkp_selected_arches(), each as hkp_add_packaging_probe(ARCH ). Register +# and UKDs (hkp_probe_derive_root.py --list-arches; wildcard descriptors name none, +# generic targets map to a member, see _hkp_probe_automatic_arches) minus +# hkp_selected_arches(), each as hkp_add_packaging_probe(ARCH ). Register # the probe tooling tests, write the manifest of ctest names # (${CMAKE_BINARY_DIR}/hkp-probes/manifest.txt) and define the aggregate target # hkp_packaging_probes. Zero probes is valid: every explicit arch is a build target, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md b/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md index c70362d790ea..429e9f92a341 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/examples/descriptors/README.md @@ -80,12 +80,11 @@ compiled artifact. Authoring it would let the descriptor disagree with the kerne authored.** `file` resolves relative to the descriptor that names it, must stay inside the source root, and has no root-relative fallback, like a hip `source`. The object is packed as-is, with no compile. This tree contains no hsaco example. -Each hsaco UKD must list in `arch` the arch(es) its object runs on (a -generic-target object lists every arch it runs on); an absent or empty `arch` -is rejected. +Each hsaco UKD needs a non-empty `arch`: [rules](../../README.md#hsaco-kernels-and-arch). **`arch` filters which shard a descriptor ships in.** It does not select a -builder: naming `gfx942` does not make a gfx950 builder produce gfx942 code. +builder: naming `gfx942` does not make a gfx950 builder produce gfx942 code. For generic +targets, see [Generic GPU targets](../../README.md#generic-gpu-targets). **`library` is relative to the descriptor that declared it**, and the archive is one per arch at the arch root — so a descriptor in a child folder climbs back out diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake index 9cda0219bcd2..288011fa3b36 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/probes.cmake @@ -13,8 +13,12 @@ # Wildcard descriptors (no `arch`, or an empty list) name no architecture and get no # automatic probe. That is safe: a wildcard ships for whatever the build targets, so # every lane's own production pack, and its GPU test lanes, exercise it on every PR. -# A non-concrete arch name (gfx9-4-generic, a TheRock family) is skipped with a STATUS -# line: no build packs for it. +# A generic target in the generic target table (gfx11-generic) is probed through its +# first member in table order (gfx1100): the packer compiles the generic once and merges +# it into each requested member's folder, so the probe for the member covers it. A build +# that targets any member of the generic (gfx1151) already compiles it and gets no probe +# for it. A TheRock family name is skipped with a STATUS line: no build packs for it. A +# `*-generic` absent from the table fails configure, as it fails the packer. # # Without UKDS a probe packs, per compile group of every KDP shipping for its ARCH, one # UKD, or for rocke enough UKDs that each value of each varying spec field is packed diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py index 3c466512cdd0..e563e8c2b554 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/probe_support.py @@ -27,6 +27,14 @@ HKP_PACK = _PKG_DIR / "tools" / "hkp_pack.py" PROBE_ASSERT = _PKG_DIR / "tools" / "hkp_probe_assert.py" PROBE_DERIVE = _PKG_DIR / "tools" / "hkp_probe_derive_root.py" +GENERIC_TARGETS_JSON = ( + _HKP_DIR.parent.parent + / "projects" + / "hipdnn" + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" +) FIXTURES = _PKG_DIR / "tests" / "fixtures" ROCKE_FIXTURE = FIXTURES / "rocke" MAIN_FIXTURE = FIXTURES / "main" @@ -65,9 +73,17 @@ def comgr_lib(): def pack_root( - src, work, rocm_kpack_dir, hipcc, comgr_lib, *, rocke=False, filters=((), ()) + src, + work, + rocm_kpack_dir, + hipcc, + comgr_lib, + *, + rocke=False, + filters=((), ()), + arch=ARCH, ): - """Pack `src` for ARCH with the real packer; return the output root. + """Pack `src` for `arch` with the real packer; return the output root. `rocke` packs with the rocKE toolchain (wheel stamp, in-tree rocke on PYTHONPATH, comgr), as a build with HIPKERNELPROVIDER_ENABLE_ROCKE=ON does; the default packs @@ -88,9 +104,11 @@ def pack_root( "--out-root", str(out), "--arches", - ARCH, + arch, "--hipcc", hipcc, + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--inter-root", str(work / "inter"), "--kpack-python-dir", @@ -142,14 +160,14 @@ def packed_root(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): return pack_root(ROCKE_FIXTURE, work, rocm_kpack_dir, hipcc, comgr_lib, rocke=True) -def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None): +def run_assert(out_root, expect_path, rocm_kpack_dir, expect_comgr=None, arch=ARCH): cmd = [ sys.executable, str(PROBE_ASSERT), "--out-root", str(out_root), "--arch", - ARCH, + arch, "--expect", str(expect_path), "--kpack-python-dir", @@ -170,6 +188,8 @@ def run_derive(src, arch, out, ukds=(), filters=((), ())): str(PROBE_DERIVE), "--from", str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--arch", arch, "--out", @@ -207,10 +227,11 @@ def derive_and_pack( *, rocke=False, filters=((), ()), + arch=ARCH, ): - """Derive `src` for ARCH, pack the derived root under the same `filters`; return + """Derive `src` for `arch`, pack the derived root under the same `filters`; return (out root, expect path). `rocke` as in pack_root.""" - r = run_derive(src, ARCH, work / "probe" / "root", ukds, filters) + r = run_derive(src, arch, work / "probe" / "root", ukds, filters) assert r.returncode == 0, r.stderr out = pack_root( work / "probe" / "root", @@ -220,5 +241,6 @@ def derive_and_pack( comgr_lib, rocke=rocke, filters=filters, + arch=arch, ) return out, work / "probe" / "expect.json" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py index eff1a46c95f1..37bb65f603fb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/rocke/test_hkp_probe_assert_rocke.py @@ -35,6 +35,7 @@ def entry(self, name=None, kind="rocke"): "kdp": KDP_IN_ARCH_DIR, "name": self.ukd_name if name is None else name, "kind": kind, + "arch": ARCH, } def write_expect(self, entries): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py index 638bf89def83..b5f162310a8b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_derive_root.py @@ -11,7 +11,7 @@ import pytest -from probe_support import MAIN_FIXTURE, PROBE_DERIVE, run_derive +from probe_support import GENERIC_TARGETS_JSON, MAIN_FIXTURE, PROBE_DERIVE, run_derive def _derive(src: Path, arch: str, out: Path, ukds=()): @@ -105,8 +105,8 @@ def _write_root(root: Path, ukds, kdp_arch=("gfx950",), kdp_file="x.kdp.json"): (root / file).write_text(json.dumps(doc) + "\n") -def _write_standalone(root: Path, ukd_id="ukd-standalone-id", arch=None): - ukd = _rocke("standalone", builder="b9", arch=arch, ukd_id=ukd_id) +def _write_standalone(root: Path, ukd_id="ukd-standalone-id", arch=("gfx950",)): + ukd = _rocke("standalone", builder="b9", arch=list(arch), ukd_id=ukd_id) (root / "s.ukd.json").write_text(json.dumps(ukd) + "\n") @@ -179,8 +179,8 @@ def test_passthrough_kind_expect_carries_authored_kernel_source(tmp_path): # One passthrough UKD per KDP (nothing compiles); it records its authored block. assert set(by_name) == {"e", "r"} assert by_name["e"]["kernel_source"] == emb["kernel_source"] - # kpack-output entries keep the three-field schema. - assert set(by_name["r"]) == {"kdp", "name", "kind"} + # kpack-output entries carry no authored block. + assert set(by_name["r"]) == {"kdp", "name", "kind", "arch"} def test_non_varying_group_keeps_first_by_name(tmp_path): @@ -301,7 +301,9 @@ def test_wildcard_kdp_ships_everywhere(tmp_path): def test_expect_records_kdp_relative_path(tmp_path): _write_root(tmp_path / "src", [_rocke("a")], kdp_file="sub/dir/x.kdp.json") expect = _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") - assert expect == [{"kdp": "sub/dir/x.kdp.json", "name": "a", "kind": "rocke"}] + assert expect == [ + {"kdp": "sub/dir/x.kdp.json", "name": "a", "kind": "rocke", "arch": "gfx950"} + ] def test_dot_prefixed_kdp_is_copied_but_not_expected(tmp_path): @@ -350,7 +352,7 @@ def test_standalone_reference_in_other_arch_kdp_is_ignored(tmp_path): _write_root( tmp_path / "src", [_rocke("a"), "ukd-standalone-id"], kdp_arch=("gfx942",) ) - _write_standalone(tmp_path / "src") + _write_standalone(tmp_path / "src", arch=("gfx942",)) assert _derive(tmp_path / "src", "gfx950", tmp_path / "w" / "root") == [] @@ -563,16 +565,25 @@ def test_rederive_follows_source_changes(tmp_path): # --- --list-arches ---------------------------------------------------------- -def _list_arches(src: Path, *filters: str): +def _list_cmd(src: Path, *filters: str, exclude=()): + cmd = [ + sys.executable, + str(PROBE_DERIVE), + "--list-arches", + "--from", + str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + *filters, + ] + for arch in exclude: + cmd += ["--exclude", arch] + return cmd + + +def _list_arches(src: Path, *filters: str, exclude=()): r = subprocess.run( - [ - sys.executable, - str(PROBE_DERIVE), - "--list-arches", - "--from", - str(src), - *filters, - ], + _list_cmd(src, *filters, exclude=exclude), capture_output=True, text=True, check=False, @@ -588,7 +599,7 @@ def test_list_arches_unions_explicit_kdp_and_ukd_arches(tmp_path): # and standalone. wild = [_rocke("b", arch=["gfx1201"]), _rocke("c"), "ukd-standalone-id"] _write_root(src, wild, kdp_arch=None, kdp_file="wild.kdp.json") - _write_standalone(src, arch=["gfx90a"]) + _write_standalone(src, arch=("gfx90a",)) assert _list_arches(src) == ["gfx1201", "gfx90a", "gfx942", "gfx950"] @@ -601,17 +612,127 @@ def test_list_arches_excludes_wildcards(tmp_path): def test_list_arches_packer_rejection_exits_2(tmp_path): src = tmp_path / "src" _write_root(src, [_rocke("a", arch=["gfx942"])]) - r = subprocess.run( - [sys.executable, str(PROBE_DERIVE), "--list-arches", "--from", str(src)], - capture_output=True, - text=True, - check=False, - ) + r = subprocess.run(_list_cmd(src), capture_output=True, text=True, check=False) assert r.returncode == 2 assert r.stderr.startswith("hkp_probe_derive:") assert "is not a subset of KDP" in r.stderr +# --- generic targets --------------------------------------------------------- +# gpu_generic_targets.json lists gfx1100 first for gfx11-generic, and gfx1151 among its +# members. +_GENERIC = "gfx11-generic" +_FIRST_MEMBER = "gfx1100" + + +def _generic_hsaco(name): + ks = {"kind": "hsaco", "file": f"{name}.hsaco", "symbol": name} + return _ukd(name, ks, [_GENERIC]) + + +def test_list_arches_maps_generic_to_its_first_member(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + assert _list_arches(src) == [f"{_FIRST_MEMBER} {_GENERIC}"] + + +def test_list_arches_drops_a_generic_when_a_member_is_a_build_target(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + _write_root( + src, [_hsaco("o", ("gfx942",))], kdp_arch=("gfx942",), kdp_file="o.kdp.json" + ) + # gfx1151 is a member that is not the first one; any member covers the generic. + assert _list_arches(src, exclude=["gfx1151"]) == ["gfx942"] + # A build target that is no member leaves the generic probed. + assert _list_arches(src, exclude=["gfx942"]) == [ + f"{_FIRST_MEMBER} {_GENERIC}", + "gfx942", + ] + + +def test_list_arches_gives_one_probe_to_a_member_that_is_also_explicit(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + _write_root( + src, + [_hsaco("m", (_FIRST_MEMBER,))], + kdp_arch=(_FIRST_MEMBER,), + kdp_file="m.kdp.json", + ) + assert _list_arches(src) == [f"{_FIRST_MEMBER} {_GENERIC}"] + + +def test_list_arches_probes_each_generic_through_its_own_first_member(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + twelve = _ukd( + "h", + {"kind": "hsaco", "file": "h.hsaco", "symbol": "h"}, + ["gfx12-generic"], + ) + _write_root(src, [twelve], kdp_arch=("gfx12-generic",), kdp_file="h.kdp.json") + assert _list_arches(src) == [ + f"gfx1100 {_GENERIC}", + "gfx1200 gfx12-generic", + ] + + +def test_list_arches_keeps_family_names_and_rejects_unknown_generics(tmp_path): + src = tmp_path / "src" + _write_root(src, [_hsaco("f", ("gfx110x-all",))], kdp_arch=("gfx110x-all",)) + # A family name is not in the table: it prints as itself for the caller to skip. + assert _list_arches(src) == ["gfx110x-all"] + + unknown = tmp_path / "unknown" + _write_root(unknown, [_hsaco("u", ("gfx99-generic",))], kdp_arch=("gfx99-generic",)) + r = subprocess.run(_list_cmd(unknown), capture_output=True, text=True, check=False) + # The packer's loader refuses a generic absent from the table. + assert r.returncode == 2 + assert "gfx99-generic" in r.stderr + assert r.stdout == "" + + +def test_derive_trims_a_generic_only_kdp_for_a_member_arch(tmp_path): + src = tmp_path / "src" + _write_root( + src, + [_generic_hsaco(n) for n in ("g_b", "g_a", "g_c")], + kdp_arch=(_GENERIC,), + kdp_file="generic.kdp.json", + ) + _write_root(src, [_hsaco("m", (_FIRST_MEMBER,))], kdp_arch=(_FIRST_MEMBER,)) + twelve = [ + _ukd(n, {"kind": "hsaco", "file": f"{n}.hsaco", "symbol": n}, ["gfx12-generic"]) + for n in ("o1", "o2") + ] + _write_root(src, twelve, kdp_arch=("gfx12-generic",), kdp_file="twelve.kdp.json") + + out = tmp_path / "w" / "root" + expect = _derive(src, _FIRST_MEMBER, out) + + # One UKD per compile group under each pass; the packed archive of the generic + # pass is the generic's, so its entry names the generic. + assert expect == [ + {"kdp": "generic.kdp.json", "name": "g_a", "kind": "hsaco", "arch": _GENERIC}, + {"kdp": "x.kdp.json", "name": "m", "kind": "hsaco", "arch": _FIRST_MEMBER}, + ] + assert _kept(out, "generic.kdp.json") == ["g_a"] + # A generic of another family does not ship for this member: copied unchanged. + assert (out / "twelve.kdp.json").read_bytes() == ( + src / "twelve.kdp.json" + ).read_bytes() + + +def test_derive_for_a_member_outside_the_generic_ignores_its_kdps(tmp_path): + src = tmp_path / "src" + _write_root(src, [_generic_hsaco("g")], kdp_arch=(_GENERIC,)) + out = tmp_path / "w" / "root" + # gfx1200 is no member of gfx11-generic: no pass for it, nothing to expect. + assert _derive(src, "gfx1200", out) == [] + assert (out / "x.kdp.json").read_bytes() == (src / "x.kdp.json").read_bytes() + + # --- disabled kinds and excluded folders ------------------------------------- def test_disabled_kind_is_neither_kept_nor_expected(tmp_path): src = tmp_path / "src" @@ -623,6 +744,8 @@ def test_disabled_kind_is_neither_kept_nor_expected(tmp_path): str(PROBE_DERIVE), "--from", str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--arch", "gfx950", "--out", @@ -655,7 +778,16 @@ def test_list_arches_ignores_content_of_disabled_kind_and_excluded_folder(tmp_pa def _derive_filtered(src, out, ukds, *filters): - cmd = [sys.executable, str(PROBE_DERIVE), "--from", str(src), "--arch", "gfx950"] + cmd = [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + "--arch", + "gfx950", + ] cmd += ["--out", str(out), *filters] for name in ukds: cmd += ["--ukd", name] @@ -701,7 +833,16 @@ def test_arch_only_a_disabled_kind_ships_for_is_not_listed_or_derived_as_empty( # empty probe for configure to reject. r = _derive_filtered(src, tmp_path / "w" / "root", [], "--disable-kind", "rocke") assert r.returncode == 0 # gfx950 keeps h - cmd = [sys.executable, str(PROBE_DERIVE), "--from", str(src), "--arch", "gfx1201"] + cmd = [ + sys.executable, + str(PROBE_DERIVE), + "--from", + str(src), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + "--arch", + "gfx1201", + ] cmd += ["--out", str(tmp_path / "w2" / "root"), "--disable-kind", "rocke"] r = subprocess.run(cmd, capture_output=True, text=True, check=False) assert r.returncode == 3, r.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py index 8bc2058baaa0..671da349eeac 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/probes/tests/test_hkp_probe_end_to_end.py @@ -19,19 +19,27 @@ derive_and_pack, hip_source, hipcc, + read_packed, rocm_kpack_dir, run_assert, + write_packed, ) # What derive keeps from the main fixture for gfx950: pointwise.kdp.json (gfx942 and # gfx950) holds two hip UKDs in one compile group and the wildcard KDP one more in # another; the gfx942-only KDPs ship nothing. _HIP_EXPECT = [ - {"kdp": "pointwise.kdp.json", "name": "PointwiseAdd f32 block64", "kind": "hip"}, + { + "kdp": "pointwise.kdp.json", + "name": "PointwiseAdd f32 block64", + "kind": "hip", + "arch": ARCH, + }, { "kdp": "pointwise_wild.kdp.json", "name": "PointwiseAdd f32 block256", "kind": "hip", + "arch": ARCH, }, ] @@ -147,3 +155,121 @@ def test_listed_ukds_pack_and_pass(tmp_path, rocm_kpack_dir, hipcc, comgr_lib): assert not (out / ARCH / "pointwise_wild.kdp.json").exists() result = run_assert(out, expect, rocm_kpack_dir) assert result.returncode == 0, result.stderr + + +# --- generic targets ---------------------------------------------------------- +# gfx11-generic's first member in the generic target table; the probe is for it. +_MEMBER = "gfx1100" +_GENERIC = "gfx11-generic" + + +def _generic_source(dst): + """A generic hsaco KDP beside a KDP for the member itself, both authored hsaco.""" + _hsaco_source(dst, arch="gfx942") + kdp = json.loads((dst / "solo.kdp.json").read_text()) + kdp["arch"] = [_GENERIC] + kdp["kernelDescriptors"][0]["arch"] = [_GENERIC] + (dst / "solo.kdp.json").write_text(json.dumps(kdp, indent=2)) + + member = json.loads(json.dumps(kdp)) + member.update(id="kdp-member", arch=[_MEMBER]) + ukd = member["kernelDescriptors"][0] + ukd.update(id="ukd-member", name="member hsaco", arch=[_MEMBER]) + ukd["metadata"]["block_size"] = 128 + (dst / "member.kdp.json").write_text(json.dumps(member, indent=2)) + return dst + + +@pytest.fixture(scope="module") +def generic_probe(tmp_path_factory, rocm_kpack_dir, hipcc, comgr_lib): + work = tmp_path_factory.mktemp("generic_probe") + return derive_and_pack( + _generic_source(work / "src"), + work, + rocm_kpack_dir, + hipcc, + comgr_lib, + arch=_MEMBER, + ) + + +def _generic_assert(out, expect, rocm_kpack_dir): + return run_assert(out, expect, rocm_kpack_dir, arch=_MEMBER) + + +@pytest.fixture +def generic_tree(generic_probe, tmp_path): + out, expect = generic_probe + copy = tmp_path / "out" + shutil.copytree(out, copy) + return copy, expect + + +def test_generic_root_expects_each_ukd_under_its_pass_and_passes( + generic_probe, rocm_kpack_dir +): + out, expect = generic_probe + assert {e["kdp"]: e["arch"] for e in json.loads(expect.read_text())} == { + "solo.kdp.json": _GENERIC, + "member.kdp.json": _MEMBER, + } + # Both archives sit in the member's folder, the generic's named by the generic. + kpacks = sorted(p.name for p in (out / _MEMBER / "kpack").iterdir()) + assert kpacks == [ + f"hip_kernel_provider_{_GENERIC}.kpack", + f"hip_kernel_provider_{_MEMBER}.kpack", + ] + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 0, result.stderr + + +def test_assert_fails_when_member_folder_lacks_the_generic_archive( + generic_tree, rocm_kpack_dir +): + out, expect = generic_tree + (out / _MEMBER / "kpack" / f"hip_kernel_provider_{_GENERIC}.kpack").unlink() + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 1 + assert f"FAIL kpack-missing: {out / _MEMBER / 'kpack'}" in result.stderr + assert f"hip_kernel_provider_{_GENERIC}.kpack" in result.stderr + + +def test_assert_fails_when_member_archive_stands_in_for_the_generic_one( + generic_tree, rocm_kpack_dir +): + # The generic UKD's blob is looked up under the generic spelling in the generic + # archive: the member's archive, copied over it, holds no such entry. + out, expect = generic_tree + kpack = out / _MEMBER / "kpack" + shutil.copyfile( + kpack / f"hip_kernel_provider_{_MEMBER}.kpack", + kpack / f"hip_kernel_provider_{_GENERIC}.kpack", + ) + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 1 + assert "FAIL kpack-toc:" in result.stderr + assert "solo.kdp.json" in result.stderr + + +@pytest.mark.parametrize( + "kdp, edit_ukd, wrong", + [ + ("solo.kdp.json", True, [_MEMBER]), + ("solo.kdp.json", False, [_MEMBER]), + ("member.kdp.json", True, [_GENERIC]), + ], +) +def test_assert_fails_when_arch_is_not_the_ukds_pass( + generic_tree, rocm_kpack_dir, kdp, edit_ukd, wrong +): + out, expect = generic_tree + path = out / _MEMBER / kdp + doc = read_packed(path) + if edit_ukd: + doc["kernelDescriptors"][0]["arch"] = wrong + else: + doc["arch"] = wrong + write_packed(path, doc) + result = _generic_assert(out, expect, rocm_kpack_dir) + assert result.returncode == 1 + assert "FAIL arch-field:" in result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py index 91ae0e693779..83e17d68a166 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/__init__.py @@ -16,9 +16,8 @@ its descriptor-relative file is packed as-is, with no compile, into the same archive, and its signature is read from the object's AMDGPU metadata, as for a compiled object. The packer does not check the object's format or target -processor. Each such UKD must list the arch(es) its object runs on (a generic-target -object lists every arch it runs on); a wildcard is rejected. The author's own load test -on the target arch is the only check; no in-tree load test covers hsaco. +processor; hsaco kernels declare `arch` and are tested on device +(descriptor-packaging/README.md, "hsaco kernels and arch"). """ from .errors import HkpPackError diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py index fe3db3b5bcc5..46b87ebca97a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/agreement.py @@ -52,10 +52,6 @@ def complete_metadata(metadata, kmd): return result -def overlap(left, right): - return not left or not right or bool(set(left) & set(right)) - - def validate_consumer(consumer, kmd): required = { "engine_id", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py index c0ba6e8a77c8..eb56f2958d1c 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptor_context.py @@ -8,6 +8,7 @@ import os from . import agreement, provenance_sidecar +from . import generic_targets as gtmod from .errors import HkpPackError _DESCRIPTOR_TYPES = ("kdp", "ukd", "kmd", "ued", "umd", "udd", "uhd") @@ -219,7 +220,19 @@ def declarations(bundles: list[Bundle], schemas: dict) -> dict: return found -def consumer_records(bundles: list[Bundle], schemas: dict, arch: str) -> dict: +def archive_arch(entry_arch: list, target: str, generic_targets) -> str: + """The arch spelling of the kpack archive for @target (a generic copy is keyed by its generic).""" + if target in entry_arch: + return target + for entry in entry_arch: + if gtmod.entry_tier(entry, target, generic_targets) == gtmod.TIER_GENERIC: + return entry + return target + + +def consumer_records( + bundles: list[Bundle], schemas: dict, arch: str, generic_targets +) -> dict: """UKD id -> canonical records over EVERY consumer inside the caller root. Shared standalone UKDs carry all KDP/engine/KMD bindings, not just the selected @@ -230,7 +243,12 @@ def consumer_records(bundles: list[Bundle], schemas: dict, arch: str) -> dict: header = {k: v for k, v in bundle.kdp_doc.items() if k != "kernelDescriptors"} header["arch"] = [arch] for entry in bundle.entries: - if entry.arch and arch not in entry.arch: + # Literal test kept: a generic copy's @arch is the generic's own name, no device. + if ( + entry.arch + and arch not in entry.arch + and not gtmod.admits_target(entry.arch, arch, generic_targets) + ): continue enclosing = bundle.kdp_doc if entry.inline else None if agreement.resolved_contract(entry.ukd, enclosing) is None: diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py index 677ce781b07c..be34f0adc78b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/descriptors.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from pathlib import Path, PureWindowsPath +from . import generic_targets as gtmod from .errors import HkpPackError KDP_TYPE = "kdp" @@ -67,6 +68,7 @@ def id(self): @dataclass class FlatInput: descriptors: list = field(default_factory=list) + generic_targets: object = None def by_type(self, dtype): return [d for d in self.descriptors if d.type == dtype] @@ -129,7 +131,14 @@ def arch_matches(kdp_doc, arch): return arch in archs -def _selected_entries(doc, arch, ukd_by_id): +def kdp_arch_matches(kdp_doc, arch, generic_targets): + """Whether a KDP is emitted into the pass for @arch (a generic pass: literal membership only).""" + if generic_targets.has(arch): + return arch in (kdp_doc.get("arch") or []) + return arch_matches(kdp_doc, arch) + + +def _selected_entries(doc, arch, ukd_by_id, generic_targets): """Yield the entries of doc that ship for arch. Yields `(entry_id, ukd_doc, sdesc)`: for a standalone-UKD id ref, the id @@ -144,7 +153,7 @@ def _selected_entries(doc, arch, ukd_by_id): generator no way to enumerate a standalone UKD no KDP references: an orphan is legal input the walk never compiles. """ - if not arch_matches(doc, arch): + if not kdp_arch_matches(doc, arch, generic_targets): return for entry in doc["kernelDescriptors"]: if isinstance(entry, str): @@ -155,16 +164,18 @@ def _selected_entries(doc, arch, ukd_by_id): yield None, entry, None -def _arch_subset_ok(ukd_arch, kdp_arch): +def _arch_subset_ok(ukd_arch, kdp_arch, generic_targets): """A UKD's arch is admissible under a referencing KDP's arch. An empty list on either side is a wildcard: a wildcard KDP admits any UKD, - and a wildcard UKD is admissible under any KDP. Two explicit lists require - the UKD's arches to be a subset of the KDP's. + and a wildcard UKD is admissible under any KDP here (a standalone wildcard UKD + under a KDP that has an `arch` is rejected separately). Two explicit lists require + the UKD's expanded device set to lie within the KDP's (a generic stands for + its members). """ if not ukd_arch or not kdp_arch: return True - return set(ukd_arch) <= set(kdp_arch) + return gtmod.covers(kdp_arch, ukd_arch, generic_targets) def kdp_survives(kdp_doc, flat, arch): @@ -175,7 +186,12 @@ def kdp_survives(kdp_doc, flat, arch): A KDP whose UKDs all filter out for this arch is dropped from the shard. Requires a validated `flat`: every by-id entry resolves. """ - return any(True for _ in _selected_entries(kdp_doc, arch, flat.ukd_by_id())) + return any( + True + for _ in _selected_entries( + kdp_doc, arch, flat.ukd_by_id(), flat.generic_targets + ) + ) def validate_hip_build(build, where): @@ -224,8 +240,9 @@ def _reject_nonbare_arch(archs, where): Mirrors the loader's isPlausibleArchBaseId (loader is authoritative): 'gfx' followed by one or more of [a-z0-9_-]. LLVM generic targets - ('gfx9-4-generic') are legal; a feature suffix ('gfx942:xnack-') is not, - since ':' is outside the set. + ('gfx9-4-generic') are well-formed here but must also be in the generic target + table (see `_reject_unknown_generics`); a feature suffix ('gfx942:xnack-') is + not, since ':' is outside the set. Fatal rather than advisory: a suffixed arch matches no shard, so the KDP prunes from every arch and the pack exits 0 having installed nothing -- @@ -521,7 +538,9 @@ def _validate_shape(desc, log=print): _validate_kmd(desc) -def load_flat_input(root, log=print, *, exclude_folders=(), disabled_kinds=()): +def load_flat_input( + root, generic_targets, log=print, *, exclude_folders=(), disabled_kinds=() +): """Load and structurally validate every *.json descriptor under a root. Walks the root recursively: a descriptor's authored subpath is meaningful @@ -615,9 +634,10 @@ def load_flat_input(root, log=print, *, exclude_folders=(), disabled_kinds=()): _validate_shape(desc, log) descriptors.append(desc) - flat = FlatInput(descriptors=descriptors) + flat = FlatInput(descriptors=descriptors, generic_targets=generic_targets) _reject_inline_standalone_collision(flat) _reject_duplicate_ids(flat) + validate_generic_arch(flat) _validate_references(flat) _warn_orphan_standalone_ukds(flat, log) if disabled_kinds: @@ -625,6 +645,116 @@ def load_flat_input(root, log=print, *, exclude_folders=(), disabled_kinds=()): return flat +def _arch_entries(doc): + """The `arch` list of a descriptor document (empty when absent).""" + return list(doc.get("arch") or []) + + +def _reject_unknown_generics(archs, file_name, table): + for entry in archs: + if gtmod.is_generic_shaped(entry) and not table.has(entry): + raise HkpPackError( + f"{file_name}: arch entry '{entry}' is a generic target name absent " + f"from the generic target table {table.path} " + f"(known: {', '.join(table.names())})" + ) + + +def _reject_mixed_generic_list(archs, file_name, table): + """A list may not hold a generic with a member it contains, nor two generics sharing a member.""" + advice = ( + "list one, or author two KDPs (an explicit override plus a generic " + "fallback are separate packs)" + ) + for i, x in enumerate(archs): + for y in archs[i + 1 :]: + if table.has(x) and table.has(y): + shared = [m for m in table.members(x) if m in table.members(y)] + if shared: + raise HkpPackError( + f"{file_name}: 'arch' lists '{x}' and '{y}': the two " + f"generics share member {shared[0]}; {advice}" + ) + continue + generic, member = (x, y) if table.has(x) else (y, x) + if table.has(generic) and member in table.members(generic): + raise HkpPackError( + f"{file_name}: 'arch' lists '{x}' and '{y}': {generic} contains " + f"{member}; {advice}" + ) + + +def validate_generic_arch(flat): + """Packer-side generic-target rules over every KDP/UKD `arch`; see descriptor-packaging/README.md.""" + table = flat.generic_targets + ukd_by_id = flat.ukd_by_id() + for desc in flat.descriptors: + if desc.type not in (KDP_TYPE, UKD_TYPE): + continue + name = desc.path.name + _reject_unknown_generics(_arch_entries(desc.doc), name, table) + _reject_mixed_generic_list(_arch_entries(desc.doc), name, table) + if desc.type == KDP_TYPE: + for entry in desc.doc.get("kernelDescriptors", []): + if isinstance(entry, dict): + _reject_unknown_generics(_arch_entries(entry), name, table) + _reject_mixed_generic_list(_arch_entries(entry), name, table) + + for kdp in flat.kdps(): + file_name = kdp.path.name + kdp_arch = _arch_entries(kdp.doc) + kdp_generics = [a for a in kdp_arch if gtmod.is_generic_shaped(a)] + for entry in kdp.doc.get("kernelDescriptors", []): + if isinstance(entry, str): + sdesc = ukd_by_id.get(entry) + if sdesc is None: + continue + ukd, standalone = sdesc.doc, sdesc + else: + ukd, standalone = entry, None + ukd_arch = _arch_entries(ukd) + ukd_generics = [a for a in ukd_arch if gtmod.is_generic_shaped(a)] + kind = ukd.get("kernel_source", {}).get("kind") + if kind == "rocke" and (kdp_generics or ukd_generics): + generic = (kdp_generics or ukd_generics)[0] + raise HkpPackError( + f"{file_name}: rocKE does not support generic targets yet " + f"(arch '{generic}')" + ) + ukd_label = f"UKD '{ukd.get('id', '?')}'" + if standalone is not None and not ukd_arch and kdp_arch: + raise HkpPackError( + f"{file_name}: standalone UKD '{ukd.get('id')}' " + f"({standalone.path.name}) has an empty 'arch' (unrestricted) " + f"but the KDP lists {kdp_arch}; a standalone UKD under a KDP " + "with an arch must declare an arch of its own" + ) + for generic in ukd_generics: + if generic not in kdp_arch: + raise HkpPackError( + f"{file_name}: {ukd_label} declares generic target " + f"'{generic}' but the KDP does not list '{generic}'; it " + "would ship in no shard" + ) + if not kdp_generics: + continue + if standalone is not None: + who = f"standalone UKD '{ukd.get('id')}' ({standalone.path.name})" + else: + who = f"inline UKD '{ukd.get('name', ukd.get('id', '?'))}'" + if standalone is None and not ukd_arch: + continue # inline UKD with no arch inherits the KDP's + lists_all = all(g in ukd_arch for g in kdp_generics) + only_listed = all(a in kdp_arch for a in ukd_arch) + if not lists_all or not only_listed: + raise HkpPackError( + f"{file_name}: KDP lists generic target(s) {kdp_generics} " + f"(arch {kdp_arch}) but {who} declares arch {ukd_arch}; a UKD under " + "a generic KDP must list every generic of the KDP and only " + "entries the KDP lists" + ) + + def _drop_disabled_kinds(flat, disabled_kinds, log): """Prune every UKD whose kind this build has no producer for, as arch pruning prunes one that does not apply. @@ -758,7 +888,9 @@ def _validate_references(flat): udoc = ukd_by_id[entry].doc else: udoc = entry - if not _arch_subset_ok(udoc.get("arch") or [], kdp_arch): + if not _arch_subset_ok( + udoc.get("arch") or [], kdp_arch, flat.generic_targets + ): raise HkpPackError( f"UKD '{udoc.get('id')}' arch {udoc.get('arch')} is not a " f"subset of KDP {kdp.path.name} arch {doc.get('arch')}" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py index aa4d06a59bf4..0f15f3ef7b27 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/desk_check.py @@ -9,9 +9,11 @@ from __future__ import annotations import collections +import json from pathlib import Path from . import agreement, descriptor_context, provenance_sidecar +from . import generic_targets as gtmod from .errors import HkpPackError from .kpack_resolver import load_kpack @@ -45,6 +47,9 @@ # identity can say so explicitly instead of silently shortening. _ABSENT = "" +#: Stands in for "any device" when a list is empty (unrestricted). +_SYNTHETIC_DEVICE = "" + def _canonical_dtype(value) -> str: """A dtype spelling reduced to the token both vocabularies mean, or the @@ -188,6 +193,13 @@ def metadata_identity_fields(kernels: list[dict]) -> tuple[str, ...]: return tuple(fields) +def load_kdp_arch(kdp_path: Path) -> list[str]: + """The arch list a `.kdp.json` declares (inherited by kernels with none).""" + return list( + json.loads(Path(kdp_path).read_text(encoding="utf-8")).get("arch") or [] + ) + + def load_variant_set( kdp_path: Path, *, provenance_root=None, descriptor_root=None ) -> tuple[list[dict], tuple[str, ...] | None]: @@ -239,6 +251,7 @@ def _payload( def compiled_agreement( kdp_path: Path, + generic_targets, kpack_python_dir=None, *, provenance_root=None, @@ -281,7 +294,9 @@ def compiled_agreement( 0, ) arch = arches[0] - all_records = descriptor_context.consumer_records(bundles, schemas, arch) + all_records = descriptor_context.consumer_records( + bundles, schemas, arch, generic_targets + ) failures: list[str] = [] unclaimed: list[str] = [] verified = 0 @@ -323,7 +338,13 @@ def compiled_agreement( f"verified against a binary" ) continue - payload = _payload(entry, arch, kpack_python_dir) + payload = _payload( + entry, + descriptor_context.archive_arch( + entry.arch or [arch], arch, generic_targets + ), + kpack_python_dir, + ) agreement.verify(kernel, records, payload) verified += 1 except HkpPackError as exc: @@ -398,25 +419,38 @@ def metadata_spec_drift(kernels: list[dict], fields=None) -> list[tuple[str, str return bad -def _reachable_together(group: list[dict]) -> int: - """The largest number of kernels in `group` one device reaches. - - A tuple shared across disjoint arches is no collision. An absent or empty - `arch` is a wildcard and counts against every arch in the group. +def _reachable_together(group: list[dict], generic_targets, kdp_arch=()) -> int: + """The largest number of kernels in `group` tying at one arch tier on one device + (the loader's `archesCompete` rule). A kernel's arch is its own list, else `kdp_arch`. """ - sets = [frozenset(k.get("arch") or ()) for k in group] - named = frozenset().union(*sets) if sets else frozenset() - if not named: + lists = [list(k.get("arch") or kdp_arch) for k in group] + if not any(lists): return len(group) - return max(sum(1 for s in sets if not s or arch in s) for arch in named) + devices: set[str] = set() + for entries in lists: + for entry in entries: + if gtmod.is_generic_shaped(entry): + if generic_targets.has(entry): + devices.update(generic_targets.members(entry)) + else: + devices.add(entry) + if not all(lists): + devices.add(_SYNTHETIC_DEVICE) + best = 0 + for device in devices: + tiers = [gtmod.list_tier(e, device, generic_targets) for e in lists] + reached = [t for t in tiers if t is not None] + if reached: + best = max(best, max(collections.Counter(reached).values())) + return best def duplicate_matcher_tuples( - kernels: list[dict], fields=DEFAULT_MATCHER_FIELDS + kernels: list[dict], fields, generic_targets, kdp_arch=() ) -> dict[tuple, int]: """Invariant 2: no two kernels may share a matcher tuple on the same arch -- one is unreachable. Returns {tuple: count} for every tuple two kernels reach - one device with, the scope the runtime refuses in. + one device at the same arch tier with, the scope the runtime refuses in. The compared set is the union of `fields` present in any kernel's metadata, never ``kernels[0]``'s, which would make the identity list-order dependent. A @@ -427,7 +461,9 @@ def duplicate_matcher_tuples( for kernel in kernels: key = tuple(kernel.get("metadata", {}).get(f, _ABSENT) for f in present) groups[key].append(kernel) - counts = {t: _reachable_together(g) for t, g in groups.items()} + counts = { + t: _reachable_together(g, generic_targets, kdp_arch) for t, g in groups.items() + } return {t: c for t, c in counts.items() if c > 1} @@ -486,6 +522,8 @@ def __init__( drift_fields=None, *, mode: str, + generic_targets, + kdp_arch=(), agreement_failures=None, agreement_unclaimed=None, agreement_verified=0, @@ -518,7 +556,9 @@ def __init__( self.drift = metadata_spec_drift(kernels, self.drift_fields) except DeskCheckNoSpecFound as exc: self.spec_drift_error = str(exc) - self.duplicate_tuples = duplicate_matcher_tuples(kernels, self.fields) + self.duplicate_tuples = duplicate_matcher_tuples( + kernels, self.fields, generic_targets, kdp_arch + ) self.toc_applicable = _field_applicable(kernels, "toc_key") self.toc_distinct, self.toc_total = ( diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py new file mode 100644 index 000000000000..16fc4cf25f45 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/generic_targets.py @@ -0,0 +1,141 @@ +"""Generic GPU target table and arch-tier algebra; see descriptor-packaging/README.md.""" + +import json +from pathlib import Path + +from .errors import HkpPackError + +TIER_EXPLICIT = 0 +TIER_GENERIC = 1 +TIER_UNRESTRICTED = 2 + +# .../dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/ +DEFAULT_TABLE_PATH = ( + Path(__file__).resolve().parents[5] + / "projects" + / "hipdnn" + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" +) + +_GENERIC_SUFFIX = "-generic" + + +class GenericTargets: + """Generic name -> member processor names.""" + + def __init__(self, path, generics): + self._path = Path(path) + self._generics = generics + + @classmethod + def load(cls, path): + path = Path(path) + try: + doc = json.loads(path.read_text(encoding="utf-8")) + if doc["schemaVersion"] != 1: + raise ValueError(f"unsupported schemaVersion {doc['schemaVersion']!r}") + generics = {n: tuple(m) for n, m in doc["generics"].items()} + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + raise HkpPackError(f"{path}: unusable generic target table: {exc!r}") + return cls(path, generics) + + @property + def path(self): + return self._path + + def has(self, name): + return name in self._generics + + def members(self, name): + """Members of generic @name; KeyError when unlisted.""" + return self._generics[name] + + def names(self): + return tuple(self._generics) + + +def is_generic_shaped(name): + """Name shape only; GenericTargets.has tests membership.""" + return name.endswith(_GENERIC_SUFFIX) + + +def _strip_features(arch): + return arch.split(":", 1)[0] + + +def _prefix_match(device, entry): + """The packer-side twin of archMatches PREFIX: @entry is @device's base id.""" + return device == entry or device.startswith(entry + ":") + + +def entry_tier(entry, device, table): + """Tier of one entry for @device, or None.""" + if is_generic_shaped(entry): + if table.has(entry) and _strip_features(device) in table.members(entry): + return TIER_GENERIC + return None + if _prefix_match(device, entry): + return TIER_EXPLICIT + return None + + +def list_tier(entries, device, table): + """Best tier of @entries for @device; None = no match.""" + if not entries: + return TIER_UNRESTRICTED + best = None + for entry in entries: + tier = entry_tier(entry, device, table) + if tier == TIER_EXPLICIT: + return tier + if tier is not None: + best = tier + return best + + +def expand(entries, table): + """Device ids @entries admits; None for an empty list.""" + if not entries: + return None + devices = set() + for entry in entries: + if is_generic_shaped(entry): + if table.has(entry): + devices.update(table.members(entry)) + else: + devices.add(entry) + return frozenset(devices) + + +def covers(outer, inner, table): + """Does @outer admit every device @inner admits?""" + expanded_outer = expand(outer, table) + expanded_inner = expand(inner, table) + if expanded_outer is None or expanded_inner is None: + return True + return expanded_inner <= expanded_outer + + +def compete(a, b, table): + """Do the lists tie at one tier on some device?""" + if not a and not b: + return True + candidates = set() + for entry in (*a, *b): + if is_generic_shaped(entry): + if table.has(entry): + candidates.update(table.members(entry)) + else: + candidates.add(entry) + for device in candidates: + tier = list_tier(a, device, table) + if tier is not None and tier == list_tier(b, device, table): + return True + return False + + +def admits_target(entries, target, table): + """Does @entries admit @target?""" + return list_tier(entries, target, table) is not None diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py index 538c9eba50bd..6678616f50d8 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/python/hkp_pack/pipeline.py @@ -17,13 +17,14 @@ from .rocke_compile import compile_rocke_variant, rocke_variant_key from .descriptors import ( KPACK_DIR_NAME, - arch_matches, _selected_entries, + kdp_arch_matches, kdp_survives, load_flat_input, reachable_generic_ids, ) from .errors import HkpPackError +from .generic_targets import GenericTargets from .kernel_signature import kernel_signature from .kpack_resolver import load_kpack @@ -125,6 +126,8 @@ class ArchResult: out_dir: Path kpack_path: Path skipped: bool = False + # Generic-pass archives copied into this member's folder. + generic_kpack_paths: tuple = () _EXC_TEXT_LIMIT = 200 @@ -313,6 +316,9 @@ def _compile_ukd_variant( "consumers": consumers, } elif kind == "hsaco": + # The packer never reads the object's target processor; hsaco kernels declare + # `arch` and are tested on device (descriptor-packaging/README.md, + # "hsaco kernels and arch"). file = ks["file"] symbol = ks["symbol"] path = resolve_hsaco_file(source_root, rel_dir, file, where) @@ -419,15 +425,25 @@ def shipped_engines(flat, arches): What configure asks before wiring a pack. Uses the same survival rule as the walk, over the same filtered input, so a root whose every arch maps to an empty list is one the pack would skip, and an engine absent for an arch is one - that shard does not ship. A KDP whose engine names no UED contributes its Id. + that shard does not ship. A shard carries what survives its own pass and what + survives the pass of each generic that `generic_passes` merges into it. A KDP + whose engine names no UED contributes its Id. """ names = {d.id: d.doc.get("name", d.id) for d in flat.by_type("ued")} + table = flat.generic_targets + generics = generic_passes(flat, arches) return { arch: sorted( { names.get(k.doc.get("engine"), k.doc.get("engine")) for k in flat.kdps() - if kdp_survives(k.doc, flat, arch) + if any( + kdp_survives(k.doc, flat, p) + for p in ( + arch, + *(g for g in generics if arch in table.members(g)), + ) + ) } ) for arch in arches @@ -440,10 +456,10 @@ def offered_engines(flat): The answer is independent of the build's arches: it is what host-side gates read, since the host must not depend on which arch content a build packs. A KDP is offered iff it survives some arch named anywhere under `flat` (a KDP or - UKD `arch` list, inline or standalone) or the wildcard sentinel `"*"`, which - `arch_matches` accepts only for wildcards. Names are built as in - `shipped_engines`, and `shipped_engines(flat, arches)` values are always a - subset of this list. + UKD `arch` list, inline or standalone; a generic's own name is such an arch) or + the wildcard sentinel `"*"`, which `arch_matches` accepts only for wildcards. + Names are built as in `shipped_engines`, and `shipped_engines(flat, arches)` + values are always a subset of this list. """ candidates = {"*"} ukd_docs = [d.doc for d in flat.ukds()] @@ -489,7 +505,9 @@ def _agreement_inputs(flat, arch): ) # The KDP declares nothing (just checked), so there is nothing for a # kernel to inherit and each speaks only for itself. - for _sid, ukd, _sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for _sid, ukd, _sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, flat.generic_targets + ): if agreement.resolved_contract(ukd) is not None: raise HkpPackError( f"UKD {ukd['id']} in {kdp.path.name}: a specialization " @@ -509,7 +527,9 @@ def _agreement_inputs(flat, arch): kmd = schemas[kmd_id] header = _kdp_header(kdp.doc) header["arch"] = [arch] - for sid, ukd, sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for sid, ukd, sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, flat.generic_targets + ): # Completion is checked against the KMD the chain actually resolved to, # so a mis-typed or missing mandatory value fails before a compile. agreement.complete_metadata(ukd["metadata"], kmd) @@ -560,7 +580,9 @@ def _prewarm_jobs(flat, source_root, arch, observation_requests=None): jobs = [] seen = set() for kdp in flat.kdps(): - for sid, ukd, sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for sid, ukd, sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, flat.generic_targets + ): rel_dir = sdesc.rel_dir if sid is not None else kdp.rel_dir vk = _variant_key_for(ukd, rel_dir) if vk is None or vk in seen: @@ -935,7 +957,7 @@ def compile_intermediate(flat, source_root, arch, hipcc, inter_arch_dir, log=pri for kdp in flat.kdps(): doc = kdp.doc - if not arch_matches(doc, arch): + if not kdp_arch_matches(doc, arch, flat.generic_targets): _write_bytes_at( inter_arch_dir, kdp.rel_dir, kdp.path.name, kdp.path.read_bytes() ) @@ -945,7 +967,9 @@ def compile_intermediate(flat, source_root, arch, hipcc, inter_arch_dir, log=pri ukds = [] entries = [] new_kds = [] - for sid, entry, sdesc in _selected_entries(new_doc, arch, ukd_by_id): + for sid, entry, sdesc in _selected_entries( + new_doc, arch, ukd_by_id, flat.generic_targets + ): if sid is not None: # A reference to a standalone UKD: compile it once per arch and # keep the string in the KDP; it ships as its own file. Listing @@ -1466,11 +1490,71 @@ def _all_ukds(): return ArchResult(arch=arch, out_dir=out_arch_dir, kpack_path=kpack_path) +def _merge_generic_into_member(generic_dir, member_dir, staging_dir): + """Merge a generic pass's tree into a member folder atomically via `staging_dir`. + + An existing destination file is skipped when byte-identical and refused when + it differs. Any failure leaves `member_dir` untouched. + """ + generic_dir, member_dir, staging_dir = ( + Path(generic_dir), + Path(member_dir), + Path(staging_dir), + ) + if staging_dir.exists(): + shutil.rmtree(staging_dir) + try: + if member_dir.exists(): + shutil.copytree(member_dir, staging_dir, copy_function=shutil.copyfile) + else: + staging_dir.mkdir(parents=True) + for src in sorted(p for p in generic_dir.rglob("*") if p.is_file()): + rel = src.relative_to(generic_dir) + dest = staging_dir / rel + if dest.exists(): + if dest.read_bytes() == src.read_bytes(): + continue + raise HkpPackError( + f"{rel.as_posix()} would be written by shard " + f"'{member_dir.name}' ({member_dir / rel}) and shard " + f"'{generic_dir.name}' ({src}) with different bytes; the " + "shards' member sets intersect" + ) + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src, dest) + if member_dir.exists(): + shutil.rmtree(member_dir) + staging_dir.rename(member_dir) + except HkpPackError: + shutil.rmtree(staging_dir, ignore_errors=True) + raise + except OSError as exc: + shutil.rmtree(staging_dir, ignore_errors=True) + raise HkpPackError( + f"cannot merge '{generic_dir.name}' into '{member_dir}': {exc}" + ) from exc + + +def generic_passes(flat, arches): + """The generics `run_pipeline` compiles for @arches, in table order. + + A generic is compiled when one of @arches is its member and a KDP ships for it. + """ + table = flat.generic_targets + return [ + generic + for generic in table.names() + if any(a in table.members(generic) for a in arches) + and any(kdp_survives(k.doc, flat, generic) for k in flat.kdps()) + ] + + def run_pipeline( source_root, arches, out_root, hipcc, + generic_targets_json, rocm_kpack_dir=None, inter_root=None, expected_sha256=None, @@ -1496,6 +1580,9 @@ def run_pipeline( kernels for , skipping'; every arch skipping packs nothing, which is a clean skip too. Empty arch list installs nothing (exit 0). + `generic_targets_json` is the generic target table; generic passes are + merged into member folders (descriptor-packaging/README.md). + `exclude_folders` names top-level child folders of the root this build does not pack, and `disabled_kinds` the kernel_source kinds it has no producer for; both are pruned at load (load_flat_input) exactly as arch pruning prunes, so @@ -1507,8 +1594,10 @@ def run_pipeline( return results kpack_mod, comp = load_kpack(rocm_kpack_dir) + generic_targets = GenericTargets.load(generic_targets_json) flat = load_flat_input( source_root, + generic_targets, log=log, exclude_folders=exclude_folders, disabled_kinds=disabled_kinds, @@ -1594,15 +1683,71 @@ def run_pipeline( arch=arch, out_dir=out_arch_dir, kpack_path=None, skipped=True ) + for generic in generic_passes(flat, arches): + members = [a for a in arches if a in generic_targets.members(generic)] + generic_out = inter_root / ".generic-out" / generic + try: + inter = compile_intermediate( + flat, source_root, generic, hipcc, inter_root / generic, log=log + ) + if generic_out.exists(): + shutil.rmtree(generic_out) + generic_result = pack_arch( + flat, + inter, + generic_out, + kpack_mod, + comp, + expected_sha256=expected_sha256, + hipcc=hipcc, + rocke_wheel_stamp=rocke_wheel_stamp, + group=group, + source_label=source_label, + ) + except HkpPackError as exc: + failures[generic] = str(exc) + log(f"ERROR: {generic} failed: {exc}") + if generic_out.exists(): + shutil.rmtree(generic_out) + continue + for member in members: + if member in failures: + continue + member_dir = out_root / member + try: + _merge_generic_into_member( + generic_out, member_dir, out_root / f".{member}.merge.staging" + ) + except HkpPackError as exc: + failures[member] = str(exc) + log(f"ERROR: {member} failed merging {generic}: {exc}") + break + previous = results.get(member) + kpack_path = None if previous is None else previous.kpack_path + copied = () + if generic_result.kpack_path is not None: + copied = ( + member_dir / KPACK_DIR_NAME / _kpack_filename(generic, group), + ) + results[member] = ArchResult( + arch=member, + out_dir=member_dir, + kpack_path=kpack_path, + skipped=False, + generic_kpack_paths=( + (() if previous is None else previous.generic_kpack_paths) + copied + ), + ) + if failures: # Non-zero exit with partial output: the build fails loudly, but a # developer can still inspect what did succeed. Exiting 0 here would # resurrect the silent-empty-package class of defect. detail = "; ".join(f"{a}: {r}" for a, r in sorted(failures.items())) raise HkpPackError( - f"packing failed for {len(failures)} of {len(arches)} arch(es) " - f"[{detail}]. Arches that succeeded were written; the failed arches' " - "output was discarded." + f"packing failed for {len(failures)} failure(s) across {len(arches)} " + f"requested arch(es) [{detail}]. Arches that succeeded were written; " + "the failed arches' output was discarded." ) # Nothing surviving any arch is a root with nothing to pack for this build -- @@ -1624,7 +1769,9 @@ def run_pipeline( for kdp in flat.kdps(): if not kdp_survives(kdp.doc, flat, arch): continue - for sid, ukd, _sdesc in _selected_entries(kdp.doc, arch, ukd_by_id): + for sid, ukd, _sdesc in _selected_entries( + kdp.doc, arch, ukd_by_id, generic_targets + ): if not _is_passthrough(ukd): raise HkpPackError( f"packing '{source_root}' wrote descriptors but no " diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/cmake_harness.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/cmake_harness.py index f7dbd91141f7..d0922e4e4bd2 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/cmake_harness.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/cmake_harness.py @@ -11,6 +11,8 @@ import sys from pathlib import Path +from pack_helpers import GENERIC_TARGETS_JSON + PKG = Path(__file__).resolve().parents[1] MODULE = PKG / "cmake" / "HkpPackaging.cmake" @@ -22,6 +24,7 @@ def consumer_preamble(project): project({project} NONE) list(APPEND CMAKE_MODULE_PATH "{(PKG.parent / 'cmake').as_posix()}") include("{MODULE.as_posix()}") +set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "{GENERIC_TARGETS_JSON.as_posix()}") """ diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json index 8e3b0f9bb1d1..23cefcd11121 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/fixtures/main/pointwise_add_b128.ukd.json @@ -2,6 +2,7 @@ "version": "0.1", "id": "ukd-pointwise-add-f32-b128", "name": "PointwiseAdd f32 block128", + "arch": ["gfx942", "gfx950"], "kernel_source": { "kind": "hip", "source": "PointwiseAdd.cpp", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/pack_helpers.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/pack_helpers.py index 8dd3e9731d04..389de1b809cb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/pack_helpers.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/pack_helpers.py @@ -16,8 +16,8 @@ import pytest -from hkp_pack import provenance_sidecar -from hkp_pack.pipeline import run_pipeline +from hkp_pack import pipeline, provenance_sidecar +from hkp_pack.generic_targets import DEFAULT_TABLE_PATH, GenericTargets PACKAGING = Path(__file__).resolve().parent.parent @@ -29,6 +29,17 @@ EXAMPLE_ROOT = PACKAGING / "examples" / "descriptors" +#: The shipped generic target table, as the path the packer and its tools take and as +#: the loaded table the library entry points take. +GENERIC_TARGETS_JSON = DEFAULT_TABLE_PATH +GENERIC_TARGETS = GenericTargets.load(GENERIC_TARGETS_JSON) + + +def run_pipeline(**kwargs): + """`pipeline.run_pipeline` with the shipped generic target table.""" + return pipeline.run_pipeline(generic_targets_json=GENERIC_TARGETS_JSON, **kwargs) + + def _read(path): return json.loads(path.read_text(encoding="utf-8")) @@ -164,7 +175,15 @@ def _run_cli(*args, mode="structural"): """The CLI as an agent runs it. The mode is always explicit, because the tool requires it.""" return subprocess.run( - [sys.executable, str(_TOOL), "--mode", mode, *args], + [ + sys.executable, + str(_TOOL), + "--mode", + mode, + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + *args, + ], capture_output=True, text=True, ) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_desk_check_rocke_corpus.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_desk_check_rocke_corpus.py index 04b6ec219fc6..68a3d2353f32 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_desk_check_rocke_corpus.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_desk_check_rocke_corpus.py @@ -27,14 +27,15 @@ symbol_distinctness, toc_key_uniqueness, ) -from hkp_pack.pipeline import run_pipeline from pack_helpers import ( _EXAMPLES, _ROOT_IDS, + GENERIC_TARGETS, _read, _require_bundles, _run_cli, read_shipped, + run_pipeline, write_shipped, ) @@ -148,7 +149,10 @@ def test_corrected_check_also_works_on_the_authored_tree(self, desk_check_fixtur class TestInvariant2DuplicateMatcherTuples: def test_distinct_variants_report_no_duplicates(self, packed_desk_check): assert ( - duplicate_matcher_tuples(_kernels(packed_desk_check), _MATCHER_FIELDS) == {} + duplicate_matcher_tuples( + _kernels(packed_desk_check), _MATCHER_FIELDS, GENERIC_TARGETS + ) + == {} ) def test_real_pack_of_two_identical_matcher_tuples_is_detected( @@ -170,7 +174,9 @@ def mutate(doc): shipped = _pack_mutated( tmp_path, desk_check_fixture, hipcc, rocm_kpack_dir, mutate ) - dupes = duplicate_matcher_tuples(_kernels(shipped), _MATCHER_FIELDS) + dupes = duplicate_matcher_tuples( + _kernels(shipped), _MATCHER_FIELDS, GENERIC_TARGETS + ) assert dupes == {(1, 64): 2}, dupes diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_layout_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_layout_rocke.py index a37871e11cfe..b971a183e55a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_layout_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_layout_rocke.py @@ -6,7 +6,6 @@ import hashlib -from hkp_pack.pipeline import run_pipeline from pack_helpers import ( ARCH, EXAMPLE_ROOT, @@ -16,6 +15,7 @@ _read, _run, read_shipped, + run_pipeline, ) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_parallel_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_parallel_rocke.py index 2dbf246338d9..1f555a3b4231 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_parallel_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_parallel_rocke.py @@ -19,6 +19,7 @@ from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError from pack_helpers import ( + GENERIC_TARGETS, _child_sys_path, _conftest_only_paths, _kdp, @@ -76,7 +77,7 @@ def test_prewarm_failure_names_variant(failing_corpus, tmp_path, monkeypatch): detector for. """ monkeypatch.setenv("HKP_PACK_JOBS", "2") - flat = load_flat_input(failing_corpus, log=_silent) + flat = load_flat_input(failing_corpus, GENERIC_TARGETS, log=_silent) jobs = pipeline._prewarm_jobs(flat, failing_corpus, TARGET_ARCH) assert len(jobs) >= 2, "a single job returns before starting a pool" @@ -228,7 +229,7 @@ def _serial_pool(monkeypatch, between=None): def _editable_jobs(corpus, out_dir): - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) jobs = pipeline._prewarm_jobs(flat, corpus, TARGET_ARCH) return flat, [replace(job, out_dir=str(out_dir), hipcc="hipcc") for job in jobs] @@ -282,7 +283,7 @@ def _edit_producer(): with pytest.raises(HkpPackError) as excinfo: pipeline.compile_intermediate( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, "hipcc", @@ -308,7 +309,7 @@ def test_pool_accepts_variants_built_by_one_producer_revision( _serial_pool(monkeypatch) inter = pipeline.compile_intermediate( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, "hipcc", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_rocke.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_rocke.py index 624f32e201b4..0377b935ca6d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_rocke.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_pack_rocke.py @@ -11,13 +11,12 @@ from synthesised_objects import _arg, _kernel, _object, requires_msgpack from hkp_pack.descriptors import load_flat_input from hkp_pack.errors import HkpPackError -from hkp_pack.pipeline import run_pipeline from hkp_pack.rocke_compile import ( build_spec, compile_rocke_variant, rocke_variant_key, ) -from pack_helpers import read_shipped +from pack_helpers import GENERIC_TARGETS, read_shipped, run_pipeline ARCH = "gfx950" @@ -461,7 +460,7 @@ def test_variant_key_stem_is_source_stem(): # --- D. rocke validation accept/reject (quick, comgr-free) ------------------ @pytest.mark.quick def test_validation_accepts_rocke_ukd(tmp_path, rocke_fixture, rocke_ukd): - flat = load_flat_input(_copy_fixture(tmp_path, rocke_fixture)) + flat = load_flat_input(_copy_fixture(tmp_path, rocke_fixture), GENERIC_TARGETS) kdp = next(k for k in flat.kdps() if k.id == "kdp-attention") ks = kdp.doc["kernelDescriptors"][0]["kernel_source"] assert ks["kind"] == "rocke" @@ -476,7 +475,7 @@ def test_validation_rejects_missing_builder(tmp_path, rocke_fixture): del doc["kernelDescriptors"][0]["kernel_source"]["builder"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'builder'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -487,7 +486,7 @@ def test_validation_rejects_missing_spec(tmp_path, rocke_fixture): del doc["kernelDescriptors"][0]["kernel_source"]["spec"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'spec'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -498,7 +497,7 @@ def test_validation_rejects_non_object_spec(tmp_path, rocke_fixture): doc["kernelDescriptors"][0]["kernel_source"]["spec"] = "not-an-object" p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="invalid spec"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -512,7 +511,7 @@ def test_validation_standalone_rocke_admitted(tmp_path, rocke_fixture): doc["kernelDescriptors"] = [standalone_id] kdp_path.write_text(json.dumps(doc), encoding="utf-8") (src / "attention.ukd.json").write_text(json.dumps(inline), encoding="utf-8") - flat = load_flat_input(src) + flat = load_flat_input(src, GENERIC_TARGETS) assert standalone_id in flat.ukd_by_id() @@ -753,7 +752,7 @@ def test_rocke_toc_key_collision_is_detected(tmp_path, monkeypatch, rocm_kpack_d (root / "collide.kdp.json").write_text(json.dumps(kdp), encoding="utf-8") with pytest.raises(HkpPackError, match="toc_key collision"): - pipeline.run_pipeline( + run_pipeline( source_root=root, arches=[ARCH], out_root=tmp_path / "out", diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_python_environment.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_python_environment.py index 4873e7e3e076..e76848822968 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_python_environment.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/rocke/test_hkp_python_environment.py @@ -15,7 +15,6 @@ from pathlib import Path import pytest - from cmake_harness import SuppliedPython, consumer_preamble, write_module pytestmark = pytest.mark.quick diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py index e58101c4db7e..03d0ae0b626a 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_desk_check_invariants.py @@ -31,8 +31,17 @@ toc_key_uniqueness, ) from hkp_pack.errors import HkpPackError -from hkp_pack.pipeline import run_pipeline -from pack_helpers import _EXAMPLES, _ROOT_IDS, _TOOL, _read, _require_bundles, _run_cli +from pack_helpers import ( + _EXAMPLES, + _ROOT_IDS, + _TOOL, + GENERIC_TARGETS, + GENERIC_TARGETS_JSON, + _read, + _require_bundles, + _run_cli, + run_pipeline, +) ARCH = "gfx950" @@ -71,19 +80,53 @@ def test_one_tuple_on_disjoint_arches_is_not_a_duplicate(self): unreachable. The runtime refuses a duplicate only on an arch both kernels reach.""" kernels = self._twins(["gfx942"], ["gfx950"]) - assert duplicate_matcher_tuples(kernels, ("dtype",)) == {} + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} def test_one_tuple_on_a_shared_arch_is_still_a_duplicate(self): """Control for the case above: arch scoping narrows the check rather than switching it off. A single overlapping arch is enough.""" kernels = self._twins(["gfx942", "gfx950"], ["gfx950"]) - assert duplicate_matcher_tuples(kernels, ("dtype",)) == {("FLOAT",): 2} + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } - def test_an_arch_less_kernel_collides_with_every_arch(self): - """An absent arch is the wildcard `arch_matches` reads it as, so it - reaches the other kernel's device and the two are a real collision.""" + def test_an_arch_less_kernel_inherits_the_kdp_arch_and_collides_with_an_explicit_one( + self, + ): + """An arch-less kernel inherits the KDP list, so it ties an explicit gfx942 one.""" kernels = self._twins(None, ["gfx942"]) - assert duplicate_matcher_tuples(kernels, ("dtype",)) == {("FLOAT",): 2} + assert duplicate_matcher_tuples( + kernels, ("dtype",), GENERIC_TARGETS, ["gfx942"] + ) == {("FLOAT",): 2} + + def test_a_tie_beaten_on_every_device_it_reaches_is_still_a_duplicate(self): + """Generic kernels outranked on every member still tie at the generic tier.""" + kernels = [ + {"name": n, "metadata": {"dtype": "FLOAT"}, "arch": a} + for n, a in ( + ("g1", ["gfx12-generic"]), + ("g2", ["gfx12-generic"]), + ("e0", ["gfx1200"]), + ("e1", ["gfx1201"]), + ) + ] + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == { + ("FLOAT",): 2 + } + + @pytest.mark.parametrize( + "left, right", + [ + (["gfx11-generic"], None), + (["gfx11-generic"], ["gfx1100"]), + (["gfx99-generic"], ["gfx99-generic"]), + ], + ids=["generic_vs_arch_less", "generic_vs_its_member", "unknown_generics"], + ) + def test_lists_that_never_tie_at_one_tier_do_not_collide(self, left, right): + """A generic outranks arch-less and loses to its own member; unknown matches nothing.""" + kernels = self._twins(left, right) + assert duplicate_matcher_tuples(kernels, ("dtype",), GENERIC_TARGETS) == {} # --------------------------------------------------------------------------- @@ -162,7 +205,9 @@ def test_the_packed_variant_set_reports_what_packing_made_of_it( assert declared == ("block_size", "dtype") assert all(k["kernel_source"].get("toc_key") for k in kernels) - assert duplicate_matcher_tuples(kernels, declared) == duplicates + assert ( + duplicate_matcher_tuples(kernels, declared, GENERIC_TARGETS) == duplicates + ) assert toc_key_uniqueness(kernels) == (distinct_toc, 2) # Both variants compile from one entry point, so they share a symbol; # invariant 4 tolerates that because toc_key tells them apart. @@ -234,7 +279,13 @@ def test_the_mode_is_required(self, tmp_path): """No default: a run whose mode is unstated cannot be read back out of a log, and the weaker result would read as the stronger.""" proc = subprocess.run( - [sys.executable, str(_TOOL), str(_variant_set(tmp_path, packed=True))], + [ + sys.executable, + str(_TOOL), + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + str(_variant_set(tmp_path, packed=True)), + ], capture_output=True, text=True, ) @@ -353,7 +404,10 @@ def _two_variants_with_a_translated_field(self): def test_narrowing_drift_fields_silences_drift_but_keeps_the_tuple(self): kernels = self._two_variants_with_a_translated_field() coupled = DeskCheckReport( - kernels, fields=("layout", "head_size"), mode="structural" + kernels, + fields=("layout", "head_size"), + mode="structural", + generic_targets=GENERIC_TARGETS, ) # The premise: with one shared list, `layout` false-positives. assert coupled.drift == [("nhwc", "layout"), ("nchw", "layout")] @@ -363,6 +417,7 @@ def test_narrowing_drift_fields_silences_drift_but_keeps_the_tuple(self): fields=("layout", "head_size"), drift_fields=("head_size",), mode="structural", + generic_targets=GENERIC_TARGETS, ) assert narrowed.drift == [], "drift comparison should have dropped layout" assert narrowed.duplicate_tuples == {}, ( @@ -379,6 +434,7 @@ def test_narrowing_drift_fields_does_not_narrow_the_matcher_tuple(self): fields=("dtype", "head_size"), drift_fields=("head_size",), mode="structural", + generic_targets=GENERIC_TARGETS, ) assert report.duplicate_tuples == {}, ( "dtype was dropped from the DRIFT comparison only -- it must " @@ -395,6 +451,7 @@ def test_a_real_duplicate_is_still_caught_with_narrowed_drift_fields(self): fields=("dtype", "head_size"), drift_fields=("head_size",), mode="structural", + generic_targets=GENERIC_TARGETS, ) assert report.duplicate_tuples == {("BFLOAT16", 64): 2} assert not report.ok @@ -407,7 +464,12 @@ def test_the_drift_default_is_wider_than_the_matcher_field_list(self): -> `return tuple(fields[:1])`.""" kernels = self._two_variants_differing_only_in_dtype() kernels[0]["metadata"]["head_size"] = 999 # real drift - report = DeskCheckReport(kernels, fields=("dtype",), mode="structural") + report = DeskCheckReport( + kernels, + fields=("dtype",), + mode="structural", + generic_targets=GENERIC_TARGETS, + ) assert report.fields == ("dtype",) assert report.drift_fields == ("dtype", "head_size") assert report.drift == [("bf16", "head_size")] @@ -434,12 +496,19 @@ def _mixed(self): ] def test_absent_field_is_distinguishing_not_a_collision(self): - assert duplicate_matcher_tuples(self._mixed(), ("head_size", "block_n")) == {} + assert ( + duplicate_matcher_tuples( + self._mixed(), ("head_size", "block_n"), GENERIC_TARGETS + ) + == {} + ) def test_result_is_independent_of_kernel_order(self): fields = ("head_size", "block_n") - forward = duplicate_matcher_tuples(self._mixed(), fields) - reverse = duplicate_matcher_tuples(list(reversed(self._mixed())), fields) + forward = duplicate_matcher_tuples(self._mixed(), fields, GENERIC_TARGETS) + reverse = duplicate_matcher_tuples( + list(reversed(self._mixed())), fields, GENERIC_TARGETS + ) assert forward == reverse == {} def test_two_kernels_both_missing_the_field_still_collide(self): @@ -447,7 +516,9 @@ def test_two_kernels_both_missing_the_field_still_collide(self): two kernels are indistinguishable to the matcher and must collide.""" kernels = self._mixed() del kernels[0]["metadata"]["block_n"] - assert duplicate_matcher_tuples(kernels, ("head_size", "block_n")) == {(64,): 2} + assert duplicate_matcher_tuples( + kernels, ("head_size", "block_n"), GENERIC_TARGETS + ) == {(64,): 2} def test_absent_marker_distinguishes_only_when_some_kernel_declares_it(self): """Complement of the case above: once any kernel declares the field, @@ -461,9 +532,9 @@ def test_absent_marker_distinguishes_only_when_some_kernel_declares_it(self): } ] # Two kernels share (64, absent); the block_n=64 one stands alone. - assert duplicate_matcher_tuples(kernels, ("head_size", "block_n")) == { - (64, ""): 2 - } + assert duplicate_matcher_tuples( + kernels, ("head_size", "block_n"), GENERIC_TARGETS + ) == {(64, ""): 2} @pytest.mark.quick @@ -550,10 +621,12 @@ def test_declared_fields_distinguish_what_the_generic_list_collapses( kdp = self._bundle(tmp_path, ("head_size", "waves_per_eu")) kernels, fields = load_variant_set(kdp) assert fields == ("head_size", "waves_per_eu") - assert duplicate_matcher_tuples(kernels, fields) == {} + assert duplicate_matcher_tuples(kernels, fields, GENERIC_TARGETS) == {} # The premise: waves_per_eu is not in the generic list, so the same # two kernels are indistinguishable under it. - assert duplicate_matcher_tuples(kernels, DEFAULT_MATCHER_FIELDS) == {(64,): 2} + assert duplicate_matcher_tuples( + kernels, DEFAULT_MATCHER_FIELDS, GENERIC_TARGETS + ) == {(64,): 2} proc = _run_cli(str(kdp)) assert proc.returncode == 0, proc.stdout + proc.stderr assert "duplicate matcher tuples: none" in proc.stdout @@ -582,9 +655,9 @@ def test_a_kernel_carrying_no_metadata_derives_an_empty_identity(self): """ assert metadata_identity_fields([{"name": "k"}]) == () assert duplicate_matcher_tuples( - [{"name": "a"}, {"name": "b"}], () + [{"name": "a"}, {"name": "b"}], (), GENERIC_TARGETS ) == duplicate_matcher_tuples( - [{"name": "a"}, {"name": "b"}], ("dtype", "batch") + [{"name": "a"}, {"name": "b"}], ("dtype", "batch"), GENERIC_TARGETS ) def test_an_explicit_field_still_outranks_the_declaration(self, tmp_path): @@ -669,7 +742,9 @@ def test_the_generic_fallback_would_also_miss_this_field(self, tmp_path): assert "block_m" not in DEFAULT_MATCHER_FIELDS assert metadata_spec_drift(kernels, DEFAULT_MATCHER_FIELDS) == [] assert metadata_spec_drift(kernels, declared) == [] - report = DeskCheckReport(kernels, fields=declared, mode="structural") + report = DeskCheckReport( + kernels, fields=declared, mode="structural", generic_targets=GENERIC_TARGETS + ) assert report.drift == [("narrow", "block_m")] def test_drift_field_still_narrows_deliberately(self, tmp_path): @@ -703,7 +778,9 @@ def test_widening_the_drift_list_does_not_widen_the_matcher_tuple(self, tmp_path kdp.write_text(json.dumps(doc)) kernels, declared = load_variant_set(kdp) - report = DeskCheckReport(kernels, fields=declared, mode="structural") + report = DeskCheckReport( + kernels, fields=declared, mode="structural", generic_targets=GENERIC_TARGETS + ) assert report.fields == self._NARROW_CONTRACT assert "block_m" in report.drift_fields assert report.drift == [] @@ -862,7 +939,7 @@ def test_structural_lookup_raises_a_named_finding(self, tmp_path): def test_full_mode_lookup_raises_a_named_finding(self, tmp_path): typo = self._typo_beside_a_real_shard(tmp_path) with pytest.raises(HkpPackError, match="typo.kdp.json"): - compiled_agreement(typo) + compiled_agreement(typo, GENERIC_TARGETS) @pytest.mark.parametrize("mode", MODES) def test_the_cli_prints_the_finding_rather_than_a_traceback(self, tmp_path, mode): diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py new file mode 100644 index 000000000000..79d3032b1dd8 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_generic_targets.py @@ -0,0 +1,106 @@ +"""The Python tier algebra over the shared generic target table.""" + +import json +from pathlib import Path + +import pytest + +from hkp_pack import generic_targets +from hkp_pack.errors import HkpPackError +from hkp_pack.generic_targets import GenericTargets + +pytestmark = pytest.mark.quick + +_SDK_DIR = Path(__file__).resolve().parents[4] / "projects" / "hipdnn" / "plugin_sdk" +_TABLE = _SDK_DIR / "data" / "gpu_generic_targets.json" + +_TIER_NAMES = { + generic_targets.TIER_EXPLICIT: "explicit", + generic_targets.TIER_GENERIC: "generic", + generic_targets.TIER_UNRESTRICTED: "unrestricted", + None: "none", +} + + +@pytest.fixture(scope="module") +def table(): + return GenericTargets.load(_TABLE) + + +@pytest.mark.parametrize( + "doc", + [ + {"schemaVersion": 1}, + {"schemaVersion": 2, "generics": {"gfx11-generic": ["gfx1100"]}}, + {"generics": []}, + {"generics": {"gfx11-generic": 5}}, + [], + ], +) +def test_rejects_a_malformed_table(tmp_path, doc): + path = tmp_path / "table.json" + path.write_text(json.dumps(doc)) + with pytest.raises(HkpPackError): + GenericTargets.load(path) + + +@pytest.mark.parametrize( + "arch, device, expect", + [ + ([], "gfx942:sramecc+:xnack-", "unrestricted"), + (["gfx1151", "gfx11-generic"], "gfx1151:sramecc+:xnack-", "explicit"), + (["gfx1151", "gfx11-generic"], "gfx1100", "generic"), + (["gfx11-generic"], "gfx1151:sramecc+", "generic"), + (["gfx11-generic"], "gfx1154", "none"), + (["gfx12-generic"], "gfx1100", "none"), + (["gfx1250"], "gfx1250-strict", "none"), + (["gfx1250-strict"], "gfx1250-strict", "explicit"), + (["gfx94"], "gfx942", "none"), + (["gfx9-4-generic", "gfx942"], "gfx942", "explicit"), + ], +) +def test_list_tier(table, arch, device, expect): + assert _TIER_NAMES[generic_targets.list_tier(arch, device, table)] == expect + + +@pytest.mark.parametrize( + "outer, inner, expect", + [ + ([], ["gfx942"], True), + (["gfx942"], [], True), + (["gfx942", "gfx950"], ["gfx942"], True), + (["gfx942"], ["gfx942", "gfx950"], False), + (["gfx11-generic"], ["gfx1100", "gfx1153"], True), + (["gfx11-generic"], ["gfx1154"], False), + (["gfx1151"], ["gfx11-generic"], False), + (["gfx11-generic"], ["gfx11-generic"], True), + (["gfx11-generic"], ["gfx12-generic"], False), + (["gfx9-4-generic"], ["gfx1151"], False), + (["gfx942", "gfx11-generic"], ["gfx942", "gfx1100"], True), + (["gfx942", "gfx11-generic"], ["gfx950", "gfx1100"], False), + ], +) +def test_covers(table, outer, inner, expect): + assert generic_targets.covers(outer, inner, table) is expect + + +@pytest.mark.parametrize( + "a, b, expect", + [ + ([], [], True), + ([], ["gfx942"], False), + (["gfx942"], ["gfx942"], True), + (["gfx942"], ["gfx950"], False), + (["gfx11-generic"], ["gfx1151"], False), + (["gfx11-generic"], [], False), + (["gfx11-generic"], ["gfx12-generic"], False), + (["gfx11-generic"], ["gfx11-generic"], True), + (["gfx1151", "gfx11-generic"], ["gfx11-generic"], True), + (["gfx11-generic"], ["gfx11-generic", "gfx942"], True), + (["gfx1151"], ["gfx11-generic", "gfx1151"], True), + (["gfx1250"], ["gfx1250-strict"], False), + ], +) +def test_compete(table, a, b, expect): + assert generic_targets.compete(a, b, table) is expect + assert generic_targets.compete(b, a, table) is expect diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py new file mode 100644 index 000000000000..fd86eaee47b5 --- /dev/null +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_generic.py @@ -0,0 +1,528 @@ +"""Generic GPU targets at pack time; see descriptor-packaging/README.md.""" + +import copy +import json +import shutil +from pathlib import Path + +import pytest + +from hkp_pack import pipeline +from hkp_pack.descriptors import load_flat_input +from hkp_pack.errors import HkpPackError +from hkp_pack.generic_targets import GenericTargets +from hkp_pack.pipeline import _merge_generic_into_member, run_pipeline +from pack_helpers import GENERIC_TARGETS, GENERIC_TARGETS_JSON + +GENERIC = "gfx11-generic" +MEMBER_A = "gfx1100" +MEMBER_B = "gfx1151" +UNSELECTED_MEMBER = "gfx1101" +CO_NAME = "HsacoFixture.co" + +_SHARED_SUFFIXES = (".kmd.json", ".ued.json", ".udd.json", ".uhd.json", ".umd.json") + + +def _read(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def _write(path, doc): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(doc, indent=2), encoding="utf-8") + + +def _load_kpack(rocm_kpack_dir): + from hkp_pack.kpack_resolver import load_kpack + + kpack, _comp = load_kpack(rocm_kpack_dir) + return kpack + + +def _embedded(uid, arch=None): + ukd = { + "version": "0.1", + "id": uid, + "name": uid, + "kernel_source": { + "kind": "embedded_source", + "source_file": "kernels/PointwiseAdd.cpp", + "entry_point": "PointwiseAdd", + }, + "metadata": {"dtype": "FLOAT", "block_size": 64}, + "priority": 0, + } + if arch is not None: + ukd["arch"] = arch + return ukd + + +_CONTRACT = { + "specialization_contract": { + "schema_version": 1, + "consumers": [ + { + "engine_id": "ued-solo", + "kmd_id": "kmd-solo", + "metadata_fields": [], + "matcher_only_fields": ["block_size", "dtype"], + "bindings": {}, + "vocabulary": {}, + } + ], + } +} + + +def _kdp(kid, arch, ukds): + return { + "version": "0.1", + "id": kid, + "name": kid, + "arch": arch, + "matchers": ["umd-solo"], + "engine": "ued-solo", + "dispatch": "udd-solo", + "kernelDescriptors": ukds, + } + + +def _root(tmp_path, empty_arch_fixture, kdps, standalone=()): + """A source root: the fixture's shared descriptors plus KDP and standalone-UKD docs by stem.""" + root = tmp_path / "src" + root.mkdir() + for path in empty_arch_fixture.iterdir(): + if path.name.endswith(_SHARED_SUFFIXES): + shutil.copyfile(path, root / path.name) + for stem, doc in dict(kdps).items(): + _write(root / f"{stem}.kdp.json", doc) + for stem, doc in dict(standalone).items(): + _write(root / f"{stem}.ukd.json", doc) + return root + + +def _pack( + root, + tmp_path, + arches, + rocm_kpack_dir, + table=GENERIC_TARGETS_JSON, + hipcc="hipcc-not-invoked", +): + return run_pipeline( + source_root=root, + arches=list(arches), + out_root=tmp_path / "out", + hipcc=hipcc, + generic_targets_json=table, + rocm_kpack_dir=rocm_kpack_dir, + inter_root=tmp_path / "inter", + source_label="generic-test", + ) + + +def _files(folder): + return { + p.relative_to(folder).as_posix(): p.read_bytes() + for p in sorted(folder.rglob("*")) + if p.is_file() + } + + +def _load(root, table=GENERIC_TARGETS): + return load_flat_input(root, table, log=lambda *_: None) + + +@pytest.mark.quick +def test_generic_entries_land_in_every_selected_member_folder_only( + tmp_path, empty_arch_fixture, rocm_kpack_dir +): + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", ["gfx942"], [_embedded("ukd-c")]), + "explicit": _kdp("kdp-explicit", [MEMBER_B], [_embedded("ukd-e")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + "wild": _kdp("kdp-wild", [], [_embedded("ukd-wild")]), + }, + ) + _pack(root, tmp_path, ["gfx942", MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + assert sorted(p.name for p in out.iterdir()) == [MEMBER_A, MEMBER_B, "gfx942"] + assert not (out / UNSELECTED_MEMBER).exists() + assert not (out / GENERIC).exists() + + for member in (MEMBER_A, MEMBER_B): + kdp = _read(out / member / "g.kdp.json") + assert kdp["arch"] == [GENERIC] + assert [u["arch"] for u in kdp["kernelDescriptors"]] == [[GENERIC]] + assert _read(out / member / "wild.kdp.json")["arch"] == [member] + assert (out / MEMBER_A / "g.kdp.json").read_bytes() == ( + out / MEMBER_B / "g.kdp.json" + ).read_bytes() + assert not (out / "gfx942" / "g.kdp.json").exists() + + assert (out / "gfx942" / "c.kdp.json").is_file() + assert not (out / MEMBER_A / "c.kdp.json").exists() + assert (out / MEMBER_B / "explicit.kdp.json").is_file() + assert not (out / MEMBER_A / "explicit.kdp.json").exists() + + shared = [p for p in root.iterdir() if p.name.endswith(_SHARED_SUFFIXES)] + assert shared + for arch in ("gfx942", MEMBER_A, MEMBER_B): + files = _files(out / arch) + for path in shared: + assert files[path.name] == path.read_bytes() + + +@pytest.mark.quick +def test_generic_entry_is_not_materialized_when_no_member_is_selected( + tmp_path, empty_arch_fixture, rocm_kpack_dir, monkeypatch +): + seen = [] + for step in ("compile_intermediate", "pack_arch"): + real = getattr(pipeline, step) + + def spy(*args, _real=real, _step=step, **kwargs): + seen.append(args[2] if _step == "compile_intermediate" else args[1].arch) + return _real(*args, **kwargs) + + monkeypatch.setattr(pipeline, step, spy) + + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", ["gfx942"], [_embedded("ukd-c")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + _pack(root, tmp_path, ["gfx942"], rocm_kpack_dir) + + out = tmp_path / "out" + assert sorted(p.name for p in out.iterdir()) == ["gfx942"] + assert not (out / "gfx942" / "g.kdp.json").exists() + assert seen + assert GENERIC not in seen + + +@pytest.mark.quick +def test_merge_collision_with_differing_bytes_is_refused_and_leaves_the_member_untouched( + tmp_path, +): + generic_dir = tmp_path / "inter" / ".generic-out" / GENERIC + member_dir = tmp_path / "out" / MEMBER_A + staging = tmp_path / "out" / f".{MEMBER_A}.merge.staging" + _write(generic_dir / "only_generic.kdp.json", {"id": "kdp-only"}) + _write(generic_dir / "dup.ukd.json", {"id": "ukd-dup", "side": "generic"}) + _write(member_dir / "dup.ukd.json", {"id": "ukd-dup", "side": "concrete"}) + _write(member_dir / "concrete_only.kdp.json", {"id": "kdp-c"}) + before = _files(member_dir) + + with pytest.raises(HkpPackError): + _merge_generic_into_member(generic_dir, member_dir, staging) + + assert _files(member_dir) == before + assert not staging.exists() + + +@pytest.mark.quick +def test_merge_copy_failure_midway_is_refused_and_leaves_the_member_untouched( + tmp_path, monkeypatch +): + generic_dir = tmp_path / "inter" / ".generic-out" / GENERIC + member_dir = tmp_path / "out" / MEMBER_A + staging = tmp_path / "out" / f".{MEMBER_A}.merge.staging" + _write(generic_dir / "first.kdp.json", {"id": "kdp-first"}) + _write(generic_dir / "second.kdp.json", {"id": "kdp-second"}) + _write(member_dir / "concrete_only.kdp.json", {"id": "kdp-c"}) + before = _files(member_dir) + + real_copyfile = shutil.copyfile + generic_copies = [] + + def failing_on_second_generic_copy(src, dst, *args, **kwargs): + if Path(src).is_relative_to(generic_dir): + generic_copies.append(src) + if len(generic_copies) == 2: + raise OSError("injected copy failure") + return real_copyfile(src, dst, *args, **kwargs) + + monkeypatch.setattr(shutil, "copyfile", failing_on_second_generic_copy) + + with pytest.raises(HkpPackError): + _merge_generic_into_member(generic_dir, member_dir, staging) + + assert len(generic_copies) == 2, "the merge no longer copies via shutil.copyfile" + assert _files(member_dir) == before + assert not staging.exists() + + +@pytest.mark.quick +@pytest.mark.parametrize( + "step, dropped, with_generic", + [ + ("compile_intermediate", (MEMBER_A,), (MEMBER_B,)), + ("pack_arch", (), ()), + ], +) +def test_a_failed_pass_leaves_no_partial_member_or_generic_copy( + tmp_path, + empty_arch_fixture, + rocm_kpack_dir, + monkeypatch, + step, + dropped, + with_generic, +): + """A failing concrete compile drops that member; a failing generic pack drops every generic copy.""" + root = _root( + tmp_path, + empty_arch_fixture, + { + "c": _kdp("kdp-c", [MEMBER_A, MEMBER_B], [_embedded("ukd-c")]), + "g": _kdp("kdp-g", [GENERIC], [_embedded("ukd-g")]), + }, + ) + real = getattr(pipeline, step) + + def failing(*args, **kwargs): + arch = args[2] if step == "compile_intermediate" else args[1].arch + if arch == (MEMBER_A if step == "compile_intermediate" else GENERIC): + raise HkpPackError("synthetic failure") + return real(*args, **kwargs) + + monkeypatch.setattr(pipeline, step, failing) + with pytest.raises(HkpPackError): + _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + for member in (MEMBER_A, MEMBER_B): + if member in dropped: + assert not (out / member).exists() + continue + assert (out / member / "c.kdp.json").is_file() + assert (out / member / "g.kdp.json").is_file() == (member in with_generic) + assert not [p for p in out.iterdir() if p.name.startswith(".")] + + +@pytest.mark.quick +def test_generic_copies_of_every_producer_are_equal_across_separate_builds( + tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir +): + """Generic copies from separate per-arch builds are identical; the build's targets never leak in.""" + hsaco = _hsaco_ukd("ukd-hsaco", [GENERIC]) + folders = {} + for member in (MEMBER_A, MEMBER_B): + base = tmp_path / member + base.mkdir() + root = _root( + base, + empty_arch_fixture, + { + "e": _kdp("kdp-e", [GENERIC], [_embedded("ukd-e")]), + "h": _kdp("kdp-h", [GENERIC], [copy.deepcopy(hsaco)]), + }, + ) + shutil.copyfile(hsaco_fixture_dir / "gfx942" / CO_NAME, root / CO_NAME) + _pack(root, base, [member], rocm_kpack_dir) + folders[member] = _files(base / "out" / member) + assert any(name.endswith(".kpack") for name in folders[MEMBER_A]) + assert folders[MEMBER_A] == folders[MEMBER_B] + + +@pytest.mark.quick +def test_hsaco_under_a_generic_packs_under_the_generic_key_into_every_member_folder( + tmp_path, empty_arch_fixture, hsaco_fixture_dir, rocm_kpack_dir +): + authored = (hsaco_fixture_dir / "gfx942" / CO_NAME).read_bytes() + ukd = _hsaco_ukd("ukd-hsaco", [GENERIC]) + root = _root(tmp_path, empty_arch_fixture, {"g": _kdp("kdp-g", [GENERIC], [ukd])}) + shutil.copyfile(hsaco_fixture_dir / "gfx942" / CO_NAME, root / CO_NAME) + results = _pack(root, tmp_path, [MEMBER_A, MEMBER_B], rocm_kpack_dir) + + out = tmp_path / "out" + shipped = _read(out / MEMBER_A / "g.kdp.json")["kernelDescriptors"][0] + ks = shipped["kernel_source"] + assert ks["kind"] == "kpack" + assert ks["library"] == f"kpack/hip_kernel_provider_{GENERIC}.kpack" + assert shipped["arch"] == [GENERIC] + for member in (MEMBER_A, MEMBER_B): + archive_path = out / member / ks["library"] + assert results[member].generic_kpack_paths == (archive_path,) + archive = _load_kpack(rocm_kpack_dir).PackedKernelArchive.read(archive_path) + assert bytes(archive.get_kernel(ks["toc_key"], GENERIC)) == authored + assert archive.get_kernel(ks["toc_key"], member) is None + assert _files(out / MEMBER_A) == _files(out / MEMBER_B) + + +def _rocke_ukd(uid, arch=None): + ukd = _embedded(uid, arch) + ukd["kernel_source"] = { + "kind": "rocke", + "source": "pkg/kernels/attention.py", + "builder": "build_attention", + "spec": {"tile": 64}, + } + return ukd + + +def _hsaco_ukd(uid, arch): + ukd = _embedded(uid, arch) + ukd["kernel_source"] = { + "kind": "hsaco", + "file": CO_NAME, + "symbol": "HsacoFixtureAdd", + } + ukd["provenance"] = copy.deepcopy(_CONTRACT) + return ukd + + +_OVERLAPPING_TABLE = { + "schemaVersion": 1, + "generics": { + "gfx11-generic": ["gfx1100", "gfx1101"], + "gfx11-b-generic": ["gfx1101", "gfx1102"], + }, +} + +# (kdp arch, UKD form: inline|standalone|rocke, UKD arch, table override) +_REJECTED = { + "generic_beside_its_member": ([GENERIC, MEMBER_B], "inline", None, None), + "two_generics_sharing_a_member": ( + ["gfx11-generic", "gfx11-b-generic"], + "inline", + None, + _OVERLAPPING_TABLE, + ), + "unknown_generic": (["gfx99-generic"], "inline", None, None), + "inline_narrower_than_generic_kdp": ([GENERIC], "inline", [MEMBER_B], None), + "inline_unknown_generic": ([GENERIC], "inline", ["gfx99-generic"], None), + "inline_generic_beside_member": ([GENERIC], "inline", [GENERIC, MEMBER_B], None), + "mixed_list_ukd_missing_the_generic": ( + ["gfx942", GENERIC], + "inline", + ["gfx942"], + None, + ), + "standalone_without_arch_under_concrete_kdp": ( + ["gfx942"], + "standalone", + None, + None, + ), + "generic_ukd_under_empty_arch_kdp": ([], "inline", [GENERIC], None), + "generic_ukd_under_kdp_not_listing_it": ([MEMBER_B], "inline", [GENERIC], None), + "rocke_inheriting_a_generic": ([GENERIC], "rocke", None, None), +} + +_ACCEPTED = { + "standalone_listing_the_generic": ([GENERIC], "standalone", [GENERIC]), + "standalone_without_arch_under_arch_less_kdp": ([], "standalone", None), + "inline_inheriting_a_concrete_kdp": (["gfx942"], "inline", None), + "inline_inheriting_a_generic_kdp": ([GENERIC], "inline", None), + "mixed_list_ukd_listing_every_entry": ( + ["gfx942", GENERIC], + "inline", + ["gfx942", GENERIC], + ), +} + + +def _arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch): + if form == "standalone": + return _root( + tmp_path, + empty_arch_fixture, + {"g": _kdp("kdp-g", kdp_arch, ["ukd-s"])}, + {"s": _embedded("ukd-s", ukd_arch)}, + ) + ukd = ( + _rocke_ukd("ukd-i", ukd_arch) + if form == "rocke" + else _embedded("ukd-i", ukd_arch) + ) + return _root(tmp_path, empty_arch_fixture, {"g": _kdp("kdp-g", kdp_arch, [ukd])}) + + +@pytest.mark.quick +@pytest.mark.parametrize("case", sorted(_REJECTED)) +def test_an_arch_rule_violation_is_an_error(tmp_path, empty_arch_fixture, case): + kdp_arch, form, ukd_arch, table_doc = _REJECTED[case] + root = _arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch) + table = GENERIC_TARGETS + if table_doc is not None: + table_path = tmp_path / "table.json" + _write(table_path, table_doc) + table = GenericTargets.load(table_path) + with pytest.raises(HkpPackError): + _load(root, table) + + +@pytest.mark.quick +@pytest.mark.parametrize("case", sorted(_ACCEPTED)) +def test_a_consistent_arch_declaration_loads(tmp_path, empty_arch_fixture, case): + kdp_arch, form, ukd_arch = _ACCEPTED[case] + _load(_arch_rule_root(tmp_path, empty_arch_fixture, kdp_arch, form, ukd_arch)) + + +def _hip_generic_root(dest, empty_arch_fixture): + shutil.copytree(empty_arch_fixture, dest) + kdp_path = dest / "solo.kdp.json" + doc = _read(kdp_path) + doc["arch"] = [GENERIC] + _write(kdp_path, doc) + return dest + + +def test_compiled_agreement_holds_on_a_generic_copy( + tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir +): + """A generic copy's literal arch names no device; `consumer_records` must still count it.""" + from hkp_pack.desk_check import compiled_agreement + + root = _hip_generic_root(tmp_path / "src", empty_arch_fixture) + _pack(root, tmp_path, [MEMBER_A], rocm_kpack_dir, hipcc=hipcc) + failures, _unclaimed, _verified = compiled_agreement( + tmp_path / "out" / MEMBER_A / "solo.kdp.json", GENERIC_TARGETS, rocm_kpack_dir + ) + assert failures == [] + + +def test_generic_hip_archive_is_keyed_by_the_generic_spelling_and_byte_identical_across_builds( + tmp_path, empty_arch_fixture, hipcc, rocm_kpack_dir, monkeypatch +): + """Separate per-arch builds (own roots, cwd, source copy) yield identical generic files.""" + builds = {} + for label, arches in ( + ("only_a", [MEMBER_A]), + ("only_b", [MEMBER_B]), + ("both", [MEMBER_A, MEMBER_B]), + ): + base = tmp_path / label + base.mkdir() + root = _hip_generic_root(base / "deeper" / label / "src", empty_arch_fixture) + (base / "cwd").mkdir() + monkeypatch.chdir(base / "cwd") + results = _pack(root, base, arches, rocm_kpack_dir, hipcc=hipcc) + if label == "only_a": + shipped = _read(base / "out" / MEMBER_A / "solo.kdp.json") + ks = shipped["kernelDescriptors"][0]["kernel_source"] + assert shipped["arch"] == [GENERIC] + assert ks["library"] == f"kpack/hip_kernel_provider_{GENERIC}.kpack" + archive = _load_kpack(rocm_kpack_dir).PackedKernelArchive.read( + base / "out" / MEMBER_A / ks["library"] + ) + assert archive.get_kernel(ks["toc_key"], GENERIC) is not None + assert archive.get_kernel(ks["toc_key"], MEMBER_A) is None + assert results[MEMBER_A].kpack_path is None + for member in arches: + builds[(label, member)] = _files(base / "out" / member) + + reference_key, reference = next(iter(builds.items())) + assert any(name.endswith(".kpack") for name in reference) + assert any(name.endswith(".kdp.json") for name in reference) + for key, files in builds.items(): + assert files == reference, f"{key} differs from {reference_key}" diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py index 2cf3e2ac44b1..50ab9e616edb 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hip.py @@ -9,8 +9,7 @@ from hkp_pack.descriptors import load_flat_input, reachable_generic_ids from hkp_pack.errors import HkpPackError from hkp_pack.kernel_signature import kernel_signature -from hkp_pack.pipeline import run_pipeline -from pack_helpers import read_shipped +from pack_helpers import GENERIC_TARGETS, read_shipped, run_pipeline ARCHES = ["gfx942", "gfx950", "gfx90a"] @@ -449,7 +448,7 @@ def test_neg_kpack_source_missing_field(tmp_path, main_fixture, field): src = _copy_fixture(tmp_path, main_fixture) _author_kpack_source(src, drop=field) with pytest.raises(HkpPackError, match=f"missing required field '{field}'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -458,7 +457,7 @@ def test_kpack_source_complete_is_accepted(tmp_path, main_fixture): # rejected for some reason of its own would read as the drop being caught. src = _copy_fixture(tmp_path, main_fixture) _author_kpack_source(src) - flat = load_flat_input(src) + flat = load_flat_input(src, GENERIC_TARGETS) kdp = next(d for d in flat.kdps() if d.path.name == "copy.kdp.json") assert kdp.doc["kernelDescriptors"][0]["kernel_source"]["kind"] == "kpack" @@ -543,7 +542,7 @@ def test_cli2_empty_gpu_targets(tmp_path, main_fixture, hipcc, rocm_kpack_dir): # --- Unit: pruning reachability + wildcard --------------------------------- @pytest.mark.quick def test_wildcard_arch_matches(main_fixture): - flat = load_flat_input(main_fixture) + flat = load_flat_input(main_fixture, GENERIC_TARGETS) wild = next(k for k in flat.kdps() if k.id == "kdp-pointwise-wild") from hkp_pack.descriptors import arch_matches @@ -769,6 +768,7 @@ def test_standalone_ukd_shared_by_two_kdps_stored_once( # and pointwise.kdp.json reference the same standalone id on gfx942. p = src / "copy.kdp.json" doc = _read(p) + doc["arch"] = ["gfx942", "gfx950"] doc["kernelDescriptors"].append(_STANDALONE_UKD_ID) p.write_text(json.dumps(doc), encoding="utf-8") # A second referencing KDP is a second CONSUMER: it resolves to its own engine @@ -810,6 +810,7 @@ def test_standalone_ukd_referenced_by_a_second_engine_without_declaring_it_fails src = _copy_fixture(tmp_path, main_fixture) p = src / "copy.kdp.json" doc = _read(p) + doc["arch"] = ["gfx942", "gfx950"] doc["kernelDescriptors"].append(_STANDALONE_UKD_ID) p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="expected exactly one"): @@ -819,15 +820,19 @@ def test_standalone_ukd_referenced_by_a_second_engine_without_declaring_it_fails def test_standalone_ukd_referenced_by_wildcard_kdp( tmp_path, main_fixture, hipcc, rocm_kpack_dir ): - # A standalone UKD referenced by a wildcard KDP (arch []) ships in every shard. src = _copy_fixture(tmp_path, main_fixture) + wild_doc = _read(src / _STANDALONE_UKD_FILE) + wild_doc["id"] = "ukd-pointwise-add-f32-b128-wild" + del wild_doc["arch"] + wild_file = "pointwise_add_b128_wild.ukd.json" + (src / wild_file).write_text(json.dumps(wild_doc), encoding="utf-8") p = src / "pointwise_wild.kdp.json" doc = _read(p) - doc["kernelDescriptors"].append(_STANDALONE_UKD_ID) + doc["kernelDescriptors"].append(wild_doc["id"]) p.write_text(json.dumps(doc), encoding="utf-8") _run(src, tmp_path, hipcc, rocm_kpack_dir, arches=["gfx942", "gfx90a"]) for arch in ("gfx942", "gfx90a"): - assert (tmp_path / "out" / arch / _STANDALONE_UKD_FILE).exists(), arch + assert (tmp_path / "out" / arch / wild_file).exists(), arch @pytest.mark.quick @@ -905,9 +910,10 @@ def test_ukd_arch_not_subset_of_kdp_hard_errors( def test_ukd_arch_subset_wildcard_combinations_accepted( tmp_path, main_fixture, hipcc, rocm_kpack_dir ): - # (a) UKD explicit subset of KDP explicit; (b) UKD wildcard + KDP explicit; - # (c) KDP wildcard + UKD explicit. Each loads without error and emits the - # standalone into the expected shard(s). + # (a) UKD explicit subset of KDP explicit; (c) KDP wildcard + UKD explicit. + # Each loads without error and emits the standalone into the expected shard(s). + # The legal wildcard-UKD shape is the inline UKD inheriting a concrete KDP's arch, + # covered by _ACCEPTED["inline_inheriting_a_concrete_kdp"]. # (a) explicit subset: b128 -> [gfx942] under pointwise KDP [gfx942, gfx950]. src_a = _copy_fixture(tmp_path / "a", main_fixture) p = src_a / _STANDALONE_UKD_FILE @@ -918,12 +924,6 @@ def test_ukd_arch_subset_wildcard_combinations_accepted( assert (tmp_path / "a" / "out" / "gfx942" / _STANDALONE_UKD_FILE).exists() assert not (tmp_path / "a" / "out" / "gfx950" / _STANDALONE_UKD_FILE).exists() - # (b) UKD wildcard (unchanged fixture) under explicit KDP -> ships in both. - src_b = _copy_fixture(tmp_path / "b", main_fixture) - _run(src_b, tmp_path / "b", hipcc, rocm_kpack_dir, arches=["gfx942", "gfx950"]) - assert (tmp_path / "b" / "out" / "gfx942" / _STANDALONE_UKD_FILE).exists() - assert (tmp_path / "b" / "out" / "gfx950" / _STANDALONE_UKD_FILE).exists() - # (c) KDP wildcard + UKD explicit: reference b128 (arch [gfx942]) from the # wildcard KDP; it ships only in gfx942. src_c = _copy_fixture(tmp_path / "c", main_fixture) @@ -1022,7 +1022,7 @@ def test_neg_duplicate_standalone_ukd_ids(tmp_path, main_fixture): dup = _read(src / _STANDALONE_UKD_FILE) (src / "dup.ukd.json").write_text(json.dumps(dup), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1033,7 +1033,7 @@ def test_neg_duplicate_kdp_ids(tmp_path, main_fixture): doc["id"] = "kdp-pointwise" # already the id of pointwise.kdp.json p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1044,7 +1044,7 @@ def test_neg_duplicate_generic_ids(tmp_path, main_fixture): doc["id"] = "kmd-pointwise" # already the id of pointwise.kmd.json p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1057,7 +1057,7 @@ def test_neg_cross_type_id_reuse_rejected(tmp_path, main_fixture): doc["id"] = "kmd-pointwise" # a KMD id, different type p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) # --- H. Version enforcement -------------------------- @@ -1069,7 +1069,7 @@ def test_neg_file_backed_missing_version(tmp_path, main_fixture): del doc["version"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'version'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1081,7 +1081,7 @@ def test_neg_malformed_version(tmp_path, main_fixture, bad): doc["version"] = bad p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="invalid version"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1093,7 +1093,7 @@ def test_neg_inline_ukd_missing_version(tmp_path, main_fixture): del doc["kernelDescriptors"][0]["version"] p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="missing required field 'version'"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) # --- I. UED scoped name ------------------------------ @@ -1105,13 +1105,13 @@ def test_neg_ued_name_not_scoped(tmp_path, main_fixture): doc["name"] = "Pointwise engine" # unscoped p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="scoped"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick def test_scoped_ued_name_loads_clean(main_fixture): # The renamed fixture UED (test_fixture:pointwise) validates without error. - flat = load_flat_input(main_fixture) + flat = load_flat_input(main_fixture, GENERIC_TARGETS) ued = next(d for d in flat.by_type("ued") if d.id == "ued-pointwise") assert ued.doc["name"] == "test_fixture:pointwise" @@ -1155,7 +1155,7 @@ def test_non_object_provenance_is_refused_at_load(tmp_path, main_fixture): doc["provenance"] = "HIJACKED" p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="provenance must be an object"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) def _share_contract_onto_kdp(kdp_path): @@ -1214,7 +1214,7 @@ def test_effective_spec_on_a_kdp_is_refused_at_load(tmp_path, main_fixture): doc["provenance"] = {"effective_spec": {"schema_version": 1}} p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="effective_spec is reserved"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) @pytest.mark.quick @@ -1227,7 +1227,7 @@ def test_a_malformed_kdp_contract_is_refused_at_load(tmp_path, main_fixture): doc["provenance"] = {"specialization_contract": {"schema_version": 1}} p.write_text(json.dumps(doc), encoding="utf-8") with pytest.raises(HkpPackError, match="specialization_contract must be"): - load_flat_input(src) + load_flat_input(src, GENERIC_TARGETS) # --- K. Drop diagnostics + arch warning -------------- @@ -1239,7 +1239,7 @@ def test_orphan_standalone_ukd_warns(tmp_path, main_fixture): orphan["id"] = "ukd-orphan" (src / "orphan.ukd.json").write_text(json.dumps(orphan), encoding="utf-8") logs = [] - load_flat_input(src, log=logs.append) + load_flat_input(src, GENERIC_TARGETS, log=logs.append) assert any("orphan.ukd.json" in m and "not referenced" in m for m in logs) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py index bb72678ce9b0..674520111578 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_hsaco.py @@ -21,8 +21,8 @@ from hkp_pack.errors import HkpPackError from hkp_pack.hsaco_source import hsaco_variant_key from hkp_pack.kernel_signature import kernel_signature -from hkp_pack.pipeline import compile_intermediate, run_pipeline -from pack_helpers import read_shipped +from hkp_pack.pipeline import compile_intermediate +from pack_helpers import GENERIC_TARGETS, read_shipped, run_pipeline ARCH = "gfx942" OTHER_ARCH = "gfx950" @@ -261,7 +261,7 @@ def test_missing_file_has_no_root_fallback( and a path leaving the root is refused.""" root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir) shutil.move(root / "solo" / CO_NAME, root / CO_NAME) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError) as excinfo: compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -274,7 +274,7 @@ def test_missing_file_has_no_root_fallback( doc = _read(kdp_path) doc["kernelDescriptors"][0]["kernel_source"]["file"] = f"../../{CO_NAME}" _write(kdp_path, doc) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="hsaco file escapes the source root"): compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -297,7 +297,7 @@ def two_files(doc, template): shutil.copyfile( hsaco_fixture_dir / ARCH / CO_NAME, root / "solo" / "other" / CO_NAME ) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) monkeypatch.setattr(pipeline, "hsaco_variant_key", lambda *a, **k: "COLLIDE") with pytest.raises(HkpPackError) as excinfo: @@ -391,7 +391,7 @@ def bound_contract(doc, template): consumer["bindings"] = {"block_size": {"field": "block_size"}} root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, bound_contract) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="a 'hsaco' source cannot fulfil"): compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -463,7 +463,7 @@ def test_symlink_leaving_the_root_is_refused( outside.mkdir() shutil.move(root / "solo" / CO_NAME, outside / CO_NAME) (root / "solo" / CO_NAME).symlink_to(outside / CO_NAME) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="hsaco file escapes the source root"): compile_intermediate(flat, root, ARCH, NO_HIPCC, tmp_path / "inter") @@ -482,7 +482,11 @@ def set_file(doc, template): with pytest.raises(HkpPackError, match="hsaco file cannot be resolved"): compile_intermediate( - load_flat_input(root), root, ARCH, NO_HIPCC, tmp_path / "inter" + load_flat_input(root, GENERIC_TARGETS), + root, + ARCH, + NO_HIPCC, + tmp_path / "inter", ) @@ -511,7 +515,7 @@ def set_field(doc, template): root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, set_field) with pytest.raises(HkpPackError, match="ukd-solo-add-f32-b64"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -522,7 +526,7 @@ def set_symbol(doc, template): root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, set_symbol) with pytest.raises(HkpPackError, match="ukd-solo-add-f32-b64"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -540,4 +544,4 @@ def drop_arch(doc, template): root = _hsaco_root(tmp_path, empty_arch_fixture, hsaco_fixture_dir, drop_arch) with pytest.raises(HkpPackError, match="ukd-solo-add-f32-b64"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py index 532648c1257d..4c3814d574c3 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_layout.py @@ -33,13 +33,14 @@ _agreement_inputs, compile_intermediate, offered_engines, - run_pipeline, shipped_engines, ) from hkp_pack.provenance_sidecar import PACKED_MARKER from pack_helpers import ( ARCH, EXAMPLE_ROOT, + GENERIC_TARGETS, + GENERIC_TARGETS_JSON, PACKAGING, ROCKE_ARCH, _load_kpack, @@ -48,6 +49,7 @@ _run, _silent, read_shipped, + run_pipeline, ) @@ -69,7 +71,7 @@ def test_discovery_is_recursive(tmp_path, main_fixture): root = tmp_path / "root" _nest(root, "hip/pointwise", main_fixture) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) assert flat.descriptors, "nested descriptors must be discovered" for d in flat.descriptors: assert d.rel_dir.as_posix() == "hip/pointwise" @@ -81,7 +83,7 @@ def test_rel_dir_is_root_relative_parent(tmp_path, main_fixture, empty_arch_fixt _nest(root, "hip/a", main_fixture) _nest(root, "rocKE/b", empty_arch_fixture) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) by_rel = {} for d in flat.descriptors: by_rel.setdefault(d.rel_dir.as_posix(), set()).add(d.path.name) @@ -111,7 +113,7 @@ def test_same_filename_in_two_folders_both_survive( src.rename(b / src.name.replace("solob.", "solo.", 1)) assert {p.name for p in a.glob("solo.*")} == {p.name for p in b.glob("solo.*")} - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) kdp_paths = {(d.rel_dir.as_posix(), d.path.name) for d in flat.kdps()} assert ("hip/a", "solo.kdp.json") in kdp_paths assert ("hip/b", "solo.kdp.json") in kdp_paths @@ -126,7 +128,7 @@ def test_duplicate_id_across_folders_rejected(tmp_path, empty_arch_fixture): _nest(root, "hip/b", empty_arch_fixture) with pytest.raises(HkpPackError, match="duplicate"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -143,7 +145,7 @@ def test_a_hidden_folder_is_skipped_and_logged(tmp_path, empty_arch_fixture): _rename_ids(hidden, "solo", "vendor") logs = [] - flat = load_flat_input(root, log=logs.append) + flat = load_flat_input(root, GENERIC_TARGETS, log=logs.append) assert {d.rel_dir.as_posix() for d in flat.descriptors} == {"hip/a"} assert not [d for d in flat.descriptors if d.path.name.startswith("vendor.")] @@ -170,7 +172,7 @@ def test_a_non_descriptor_json_is_skipped_and_logged(tmp_path, empty_arch_fixtur incidental.write_text("[]", encoding="utf-8") logs = [] - flat = load_flat_input(root, log=logs.append) + flat = load_flat_input(root, GENERIC_TARGETS, log=logs.append) assert {d.rel_dir.as_posix() for d in flat.descriptors} == {"hip/a"} assert not [d for d in flat.descriptors if d.path.name == incidental.name] @@ -395,7 +397,7 @@ def crash(flat, inter, out_arch_dir, *a, **kw): out_root = tmp_path / "out" with pytest.raises(RuntimeError, match="uncaught failure"): - pipeline.run_pipeline( + run_pipeline( source_root=root, arches=[ARCH], out_root=out_root, @@ -444,7 +446,7 @@ def flaky_pack(flat, inter, out_arch_dir, *a, **kw): out_root = tmp_path / "out" with pytest.raises(HkpPackError, match="gfx950"): - pipeline.run_pipeline( + run_pipeline( source_root=root, arches=["gfx942", "gfx950"], out_root=out_root, @@ -483,7 +485,7 @@ def test_failed_arch_removes_its_previous_good_output( _nest(root, "hip/pointwise", main_fixture) out_root = tmp_path / "out" - pipeline.run_pipeline( + run_pipeline( source_root=root, arches=[ARCH], out_root=out_root, @@ -500,7 +502,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): monkeypatch.setattr(pipeline, "pack_arch", always_fail) with pytest.raises(HkpPackError, match=ARCH): - pipeline.run_pipeline( + run_pipeline( source_root=root, arches=[ARCH], out_root=out_root, @@ -529,7 +531,7 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): monkeypatch.setattr(pipeline, "pack_arch", always_fail) with pytest.raises(HkpPackError) as exc: - pipeline.run_pipeline( + run_pipeline( source_root=root, arches=["gfx942", "gfx950"], out_root=tmp_path / "out", @@ -540,7 +542,6 @@ def always_fail(flat, inter, out_arch_dir, *a, **kw): message = str(exc.value) assert "gfx942" in message and "gfx950" in message - assert "2 of 2" in message # --- E. The shipped example tree ------------------------------------------- @@ -551,7 +552,7 @@ def test_example_tree_is_self_consistent(): Catches a broken example on any box, including one with no toolchain, so the tree cannot rot silently between full runs. """ - flat = load_flat_input(EXAMPLE_ROOT) + flat = load_flat_input(EXAMPLE_ROOT, GENERIC_TARGETS) ids = [d.id for d in flat.descriptors] assert len(ids) == len(set(ids)), "duplicate descriptor ids in the example" @@ -790,7 +791,7 @@ def test_authored_kpack_folder_is_rejected(tmp_path, empty_arch_fixture): _nest(root, "kpack", empty_arch_fixture) with pytest.raises(HkpPackError, match="reserved"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -800,7 +801,7 @@ def test_kpack_folder_rejected_only_at_the_arch_root(tmp_path, empty_arch_fixtur root = tmp_path / "root" _nest(root, "hip/kpack", empty_arch_fixture) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) assert {d.rel_dir.as_posix() for d in flat.kdps()} == {"hip/kpack"} @@ -1036,7 +1037,7 @@ def test_generic_descriptors_are_validated_against_the_loader_schema( target.write_text(json.dumps(doc, indent=2), encoding="utf-8") with pytest.raises(HkpPackError, match=expected): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1056,7 +1057,7 @@ def test_reserved_kpack_folder_is_case_insensitive( _nest(root, spelling, empty_arch_fixture) with pytest.raises(HkpPackError, match="reserved"): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1215,7 +1216,7 @@ def test_embedded_source_root_loads(tmp_path, empty_arch_fixture): """The walk accepts the kind and leaves the block unmodified.""" root = _embedded_source_root(tmp_path, empty_arch_fixture, dict(_EMBEDDED_SOURCE)) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) kdps = list(flat.kdps()) assert len(kdps) == 1 ukd = kdps[0].doc["kernelDescriptors"][0] @@ -1232,7 +1233,7 @@ def test_embedded_source_requires_source_file_and_entry_point( root = _embedded_source_root(tmp_path, empty_arch_fixture, kernel_source) with pytest.raises(HkpPackError, match=missing): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1250,7 +1251,7 @@ def test_unhandled_kind_aborts_the_walk_and_lists_the_accepted_kinds( ) with pytest.raises(HkpPackError) as excinfo: - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) message = str(excinfo.value) assert "unsupported kind 'embedded_sources'" in message @@ -1286,7 +1287,7 @@ def test_a_kind_no_producer_handles_fails_the_compile( root = _embedded_source_root( tmp_path, empty_arch_fixture, _UNPRODUCED_SOURCES[kind] ) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError) as excinfo: compile_intermediate(flat, root, ARCH, "hipcc-not-invoked", tmp_path / "inter") @@ -1318,7 +1319,7 @@ def test_a_passthrough_kind_carries_no_specialization_obligation( root = _inline_ukd_root( tmp_path, empty_arch_fixture, _embedded_source_without_contract ) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) assert _agreement_inputs(flat, ARCH) == ({}, {}) @@ -1339,7 +1340,7 @@ def test_a_compiling_kind_without_a_contract_is_still_refused( refused before a compiler is reached. """ root = _inline_ukd_root(tmp_path, empty_arch_fixture, _drop_specialization_contract) - flat = load_flat_input(root) + flat = load_flat_input(root, GENERIC_TARGETS) with pytest.raises(HkpPackError, match="missing/invalid specialization_contract"): compile_intermediate(flat, root, ARCH, "hipcc-not-invoked", tmp_path / "inter") @@ -1363,7 +1364,7 @@ def test_embedded_source_rejects_a_parent_segment( ) with pytest.raises(HkpPackError, match=re.escape(source_file)): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) @pytest.mark.quick @@ -1383,7 +1384,7 @@ def test_embedded_source_rejects_an_absolute_path( ) with pytest.raises(HkpPackError, match=re.escape(source_file)): - load_flat_input(root) + load_flat_input(root, GENERIC_TARGETS) # --- J. Emitting embedded_source through pass-through and pruning ----------- @@ -1448,19 +1449,18 @@ def _make_embedded(folder, arch=None): kdp["kernelDescriptors"] = [inline, _STANDALONE_ID] kdp_path.write_text(json.dumps(kdp, indent=2) + "\n", encoding="utf-8") + standalone = { + "version": "0.1", + "id": _STANDALONE_ID, + "name": "PointwiseMul f32 block64 (solo)", + "kernel_source": dict(_STANDALONE_SOURCE), + "metadata": {"dtype": "FLOAT", "block_size": 64}, + "priority": 0, + } + if arch: + standalone["arch"] = list(arch) (folder / _STANDALONE_FILE).write_text( - json.dumps( - { - "version": "0.1", - "id": _STANDALONE_ID, - "name": "PointwiseMul f32 block64 (solo)", - "kernel_source": dict(_STANDALONE_SOURCE), - "metadata": {"dtype": "FLOAT", "block_size": 64}, - "priority": 0, - }, - indent=2, - ) - + "\n", + json.dumps(standalone, indent=2) + "\n", encoding="utf-8", ) return folder @@ -1908,6 +1908,8 @@ def _hkp_pack_cli(root, tmp_path, arch, rocm_kpack_dir, *flags): arch, "--hipcc", "hipcc-not-invoked", + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--inter-root", str(tmp_path / "inter"), "--source-label", @@ -2077,12 +2079,14 @@ def test_a_disabled_kind_is_pruned_from_its_kdp_and_the_rest_ships( its other entries and still survives every arch they cover.""" root, kdp_path = _mixed_root(tmp_path, main_fixture, rocke_fixture, form) - enabled = load_flat_input(root, log=_silent) + enabled = load_flat_input(root, GENERIC_TARGETS, log=_silent) (kdp,) = [k for k in enabled.kdps() if k.path == kdp_path] assert _ROCKE_UKD_ID in _entry_ids(kdp), "the premise: rocKE UKD selected" logs = [] - flat = load_flat_input(root, log=logs.append, disabled_kinds=("rocke",)) + flat = load_flat_input( + root, GENERIC_TARGETS, log=logs.append, disabled_kinds=("rocke",) + ) assert any("pointwise.kdp.json" in m and "rocke" in m for m in logs) if form == "standalone": assert any(_ROCKE_UKD_ID in m and "rocke" in m for m in logs) @@ -2112,10 +2116,12 @@ def test_a_kdp_left_with_no_enabled_kind_does_not_ship( kdp_path.write_text(json.dumps(doc, indent=2), encoding="utf-8") _add_rocke_ukd(kdp_path, ukd, form) - assert shipped_engines(load_flat_input(root, log=_silent), [ROCKE_ARCH]) == { - ROCKE_ARCH: ["test_fixture:attention"] - }, "the premise: it ships with rocKE" - flat = load_flat_input(root, log=_silent, disabled_kinds=("rocke",)) + assert shipped_engines( + load_flat_input(root, GENERIC_TARGETS, log=_silent), [ROCKE_ARCH] + ) == {ROCKE_ARCH: ["test_fixture:attention"]}, "the premise: it ships with rocKE" + flat = load_flat_input( + root, GENERIC_TARGETS, log=_silent, disabled_kinds=("rocke",) + ) assert shipped_engines(flat, [ROCKE_ARCH]) == {ROCKE_ARCH: []} results = run_pipeline( @@ -2141,11 +2147,13 @@ def test_an_excluded_folder_is_never_read(tmp_path, main_fixture): (family / "broken.kdp.json").write_text("{", encoding="utf-8") with pytest.raises(HkpPackError, match="malformed"): - load_flat_input(root, log=_silent) + load_flat_input(root, GENERIC_TARGETS, log=_silent) with pytest.raises(HkpPackError, match="malformed"): - load_flat_input(root, log=_silent, exclude_folders=("rocKE",)) + load_flat_input(root, GENERIC_TARGETS, log=_silent, exclude_folders=("rocKE",)) (root / "hip" / "rocKE" / "broken.kdp.json").unlink() - flat = load_flat_input(root, log=_silent, exclude_folders=("rocKE",)) + flat = load_flat_input( + root, GENERIC_TARGETS, log=_silent, exclude_folders=("rocKE",) + ) assert not any("rocKE" in k.path.parts for k in flat.kdps()) assert flat.kdps() @@ -2156,7 +2164,7 @@ def test_disabling_an_unknown_kind_is_an_error(tmp_path, main_fixture): root = tmp_path / "root" _nest(root, "hip/pointwise", main_fixture) with pytest.raises(HkpPackError, match="unknown kernel_source kind"): - load_flat_input(root, log=_silent, disabled_kinds=("rokce",)) + load_flat_input(root, GENERIC_TARGETS, log=_silent, disabled_kinds=("rokce",)) def _wildcard_solo(tmp_path, empty_arch_fixture, entry_arch): @@ -2188,21 +2196,27 @@ def test_offered_engines_ignores_the_build_arches_but_honours_the_filters( if root_kind == "rocke": root = tmp_path / "root" shutil.copytree(rocke_fixture, root) - flat = load_flat_input(root, log=_silent) + flat = load_flat_input(root, GENERIC_TARGETS, log=_silent) assert "test_fixture:attention" in offered_engines(flat) assert ( "test_fixture:attention" not in shipped_engines(flat, ["gfx90a"])["gfx90a"] ) - flat = load_flat_input(root, log=_silent, disabled_kinds=("rocke",)) + flat = load_flat_input( + root, GENERIC_TARGETS, log=_silent, disabled_kinds=("rocke",) + ) assert offered_engines(flat) == [] fam = tmp_path / "fam" _nest(fam, "rocKE", rocke_fixture) - flat = load_flat_input(fam, log=_silent, exclude_folders=("rocKE",)) + flat = load_flat_input( + fam, GENERIC_TARGETS, log=_silent, exclude_folders=("rocKE",) + ) assert offered_engines(flat) == [] return entry_arch = None if root_kind == "all-wildcard" else [ROCKE_ARCH] root = _wildcard_solo(tmp_path, empty_arch_fixture, entry_arch) - assert offered_engines(load_flat_input(root, log=_silent)) == ["test_fixture:solo"] + assert offered_engines(load_flat_input(root, GENERIC_TARGETS, log=_silent)) == [ + "test_fixture:solo" + ] diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py index 3adc510be263..f9087cf149ca 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_hkp_pack_parallel.py @@ -23,6 +23,7 @@ from hkp_pack.errors import HkpPackError from hkp_pack.hip_compile import hip_source_relpath, hip_variant_key from pack_helpers import ( + GENERIC_TARGETS, _child_sys_path, _conftest_only_paths, _kdp, @@ -189,7 +190,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_wild.ukd.json", - _ukd("ukd-standalone-wild", _hip_ks(_K1_SOURCE, "K1", 1024)), + _ukd("ukd-standalone-wild", _hip_ks(_K1_SOURCE, "K1", 1024), [OTHER_ARCH]), ) # Case 2 -- inline hip. @@ -238,7 +239,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_hip.ukd.json", - _ukd("ukd-standalone-hip", _hip_ks(_K1_SOURCE, "K1", 256)), + _ukd("ukd-standalone-hip", _hip_ks(_K1_SOURCE, "K1", 256), [TARGET_ARCH]), ) # Case 6 -- a standalone rocke UKD referenced by id. @@ -251,7 +252,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_rocke.ukd.json", - _ukd("ukd-standalone-rocke", _rocke_ks()), + _ukd("ukd-standalone-rocke", _rocke_ks(), [TARGET_ARCH]), ) # Case 7 -- a standalone UKD whose own arch excludes the target, listed @@ -306,7 +307,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_shared.ukd.json", - _ukd("ukd-standalone-shared", _hip_ks(_K2_SOURCE, "K2", 512)), + _ukd("ukd-standalone-shared", _hip_ks(_K2_SOURCE, "K2", 512), [TARGET_ARCH]), ) # Case 10 -- an orphan standalone UKD. @@ -354,7 +355,7 @@ def _write_corpus(dest, *, hip_only=False, with_embedded=False): _write_json( dest, "u_standalone_embedded.ukd.json", - _ukd("ukd-standalone-embedded", _embedded_ks("E1B")), + _ukd("ukd-standalone-embedded", _embedded_ks("E1B"), [TARGET_ARCH]), ) return dest @@ -423,11 +424,13 @@ def _entry_identity(entry_id, ukd_doc, sdesc): def _observed_sequence(corpus_dir): - flat = load_flat_input(corpus_dir, log=_silent) + flat = load_flat_input(corpus_dir, GENERIC_TARGETS, log=_silent) ukd_by_id = flat.ukd_by_id() observed = [] for kdp in flat.kdps(): - for tup in pipeline._selected_entries(kdp.doc, TARGET_ARCH, ukd_by_id): + for tup in pipeline._selected_entries( + kdp.doc, TARGET_ARCH, ukd_by_id, GENERIC_TARGETS + ): observed.append(_entry_identity(*tup)) return observed @@ -473,7 +476,7 @@ def test_prewarm_jobs_are_deduped_on_variant_key(corpus): one standalone UKD from two KDPs, so a job list that failed to dedup would be longer than the set of keys it carries. """ - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) jobs = pipeline._prewarm_jobs(flat, corpus, TARGET_ARCH) assert jobs, "the corpus selects variants, so the job list cannot be empty" assert len({j.vk for j in jobs}) == len(jobs) @@ -675,7 +678,7 @@ def test_prewarm_skips_hsaco_kind(hsaco_corpus): `_variant_key_for` declines the kind, so the prewarm drops it and only the compilable hip sibling is scheduled; the walk keys the hsaco UKD itself. """ - flat = load_flat_input(hsaco_corpus, log=_silent) + flat = load_flat_input(hsaco_corpus, GENERIC_TARGETS, log=_silent) hsaco_ukd = flat.kdps()[0].doc["kernelDescriptors"][0] assert pipeline._variant_key_for(hsaco_ukd, Path(".")) is None @@ -734,7 +737,7 @@ def test_prewarm_pool_stops_at_first_failure(tmp_path, monkeypatch): jobs = [_synthetic_job(corpus, 64 + i) for i in range(24)] monkeypatch.setattr(pipeline, "_prewarm_jobs", lambda *_a, **_k: list(jobs)) - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) tally = tmp_path / "tally" tally.mkdir() @@ -973,7 +976,7 @@ def test_the_pool_names_the_first_failure_in_walk_order(tmp_path, monkeypatch): """ corpus = _write_corpus(tmp_path / "two-failures", hip_only=True) monkeypatch.setenv("HKP_PACK_JOBS", "2") - flat = load_flat_input(corpus, log=_silent) + flat = load_flat_input(corpus, GENERIC_TARGETS, log=_silent) first, second = pipeline._prewarm_jobs(flat, corpus, TARGET_ARCH)[:2] hipcc = _stub_hipcc( @@ -1022,7 +1025,7 @@ def test_prewarm_pool_populates_both_caches(tmp_path, monkeypatch): variant_symbol = {} variant_observations = {} pipeline._prewarm_variants( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, _stub_hipcc(tmp_path), @@ -1035,7 +1038,7 @@ def test_prewarm_pool_populates_both_caches(tmp_path, monkeypatch): ) jobs = pipeline._prewarm_jobs( - load_flat_input(corpus, log=_silent), corpus, TARGET_ARCH + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH ) expected_symbol = {job.vk: job.ukd["kernel_source"]["entry"] for job in jobs} assert len(expected_symbol) == HIP_ONLY_EXPECTED_CO_COUNT @@ -1058,7 +1061,7 @@ def test_prewarm_pool_populates_both_caches(tmp_path, monkeypatch): def _staged_tree(corpus, out_dir): """Every staged file under `out_dir`, keyed by path relative to it.""" pipeline.compile_intermediate( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, _stub_hipcc(out_dir.parent), @@ -1153,7 +1156,7 @@ def _no_pool(*_args, **_kwargs): variant_co = {} pipeline._prewarm_variants( - load_flat_input(corpus, log=_silent), + load_flat_input(corpus, GENERIC_TARGETS, log=_silent), corpus, TARGET_ARCH, _stub_hipcc(tmp_path), diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_provenance_sidecar.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_provenance_sidecar.py index d75d650f55cd..5b25e39e707d 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_provenance_sidecar.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_provenance_sidecar.py @@ -16,7 +16,7 @@ from hkp_pack import descriptor_context, pipeline, provenance_sidecar from hkp_pack.errors import HkpPackError -from pack_helpers import is_compact, write_shipped +from pack_helpers import GENERIC_TARGETS_JSON, is_compact, write_shipped SHA = "a" * 64 OTHER_SHA = "b" * 64 @@ -645,7 +645,15 @@ def test_desk_check_fails_a_kdp_whose_sidecar_does_not_bind(tmp_path, mode): kdp = tmp_path / "shard.kdp.json" write_shipped(kdp, {"id": "pack-id", "kernelDescriptors": [_embedded_ukd("k0")]}) control = subprocess.run( - [sys.executable, str(_DESK_CHECK), "--mode", "structural", str(kdp)], + [ + sys.executable, + str(_DESK_CHECK), + "--mode", + "structural", + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + str(kdp), + ], capture_output=True, text=True, ) @@ -654,7 +662,15 @@ def test_desk_check_fails_a_kdp_whose_sidecar_does_not_bind(tmp_path, mode): ) result = subprocess.run( - [sys.executable, str(_DESK_CHECK), "--mode", mode, str(kdp)], + [ + sys.executable, + str(_DESK_CHECK), + "--mode", + mode, + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), + str(kdp), + ], capture_output=True, text=True, ) @@ -683,6 +699,8 @@ def run(*flags): str(_DESK_CHECK), "--mode", "structural", + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), *flags, str(kdp), ], @@ -716,6 +734,8 @@ def test_desk_check_refuses_a_provenance_root_for_an_unmarked_kdp(tmp_path): str(_DESK_CHECK), "--mode", "structural", + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), "--provenance-root", str(tmp_path / "provenance"), str(kdp), diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py index fabaad7ab6e4..4a815249e18f 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tests/test_specialization_agreement.py @@ -29,7 +29,7 @@ from hkp_pack.errors import HkpPackError from hkp_pack.desk_check import compiled_agreement, load_kernels from hkp_pack.kpack_resolver import load_kpack -from pack_helpers import write_shipped +from pack_helpers import GENERIC_TARGETS, GENERIC_TARGETS_JSON, write_shipped ARCH = "gfx942" PAYLOAD = b"\x7fELF-not-really-a-code-object" @@ -1015,7 +1015,7 @@ def retire_evidence(self, origin_kind): def assert_qualified_waiver(self): """Both readers pass, and both say out loud that they bound nothing.""" failures, unclaimed, verified = compiled_agreement( - self.kdp_path, self.python_dir + self.kdp_path, GENERIC_TARGETS, self.python_dir ) assert failures == [] and len(unclaimed) == 1 and verified == 0 for result in self.run_clis(): @@ -1033,6 +1033,8 @@ def run_clis(self): "full", "--field", "head_size", + "--generic-targets-json", + str(GENERIC_TARGETS_JSON), ] variant = [ sys.executable, @@ -1054,14 +1056,18 @@ def run_clis(self): ] def assert_agreement(self): - assert compiled_agreement(self.kdp_path, self.python_dir) == ([], [], 1) + assert compiled_agreement(self.kdp_path, GENERIC_TARGETS, self.python_dir) == ( + [], + [], + 1, + ) for result in self.run_clis(): assert result.returncode == 0, result.stdout + result.stderr assert "NOT VERIFIED HERE" not in result.stdout def assert_failure(self, reason): failures, unclaimed, verified = compiled_agreement( - self.kdp_path, self.python_dir + self.kdp_path, GENERIC_TARGETS, self.python_dir ) assert failures and not unclaimed and verified == 0 assert any(reason in failure for failure in failures), failures @@ -1125,7 +1131,9 @@ def test_shared_standalone_requires_every_consumer( artifact = reader_artifact(shared=True, distinct_kmd=distinct_kmd) artifact.assert_agreement() sibling_path = artifact.root / "sibling.kdp.json" - assert compiled_agreement(sibling_path, artifact.python_dir) == ([], [], 1) + assert compiled_agreement( + sibling_path, GENERIC_TARGETS, artifact.python_dir + ) == ([], [], 1) if mutation == "missing-record": artifact.ukd["provenance"]["effective_spec"]["consumers"].pop() else: @@ -1148,7 +1156,7 @@ def test_nested_standalone_reads_its_own_named_archive( wrong_origin.parent.mkdir(parents=True, exist_ok=True) artifact.archive_path.rename(wrong_origin) failures, unclaimed, verified = compiled_agreement( - artifact.kdp_path, artifact.python_dir + artifact.kdp_path, GENERIC_TARGETS, artifact.python_dir ) assert failures and not unclaimed and verified == 0 for result in artifact.run_clis(): @@ -1215,7 +1223,7 @@ def test_standalone_requires_own_declaration_despite_kdp_contracts( kdp["provenance"] = {"specialization_contract": contract} artifact.save() failures, unclaimed, verified = compiled_agreement( - artifact.kdp_path, artifact.python_dir + artifact.kdp_path, GENERIC_TARGETS, artifact.python_dir ) assert failures and not unclaimed and verified == 0 assert any("missing/invalid specialization_contract" in f for f in failures) @@ -1237,7 +1245,7 @@ def test_own_override_cannot_borrow_kdp_consumer(self, reader_artifact): ) artifact.save() with pytest.raises(HkpPackError, match="0 specialization consumers"): - compiled_agreement(artifact.kdp_path, artifact.python_dir) + compiled_agreement(artifact.kdp_path, GENERIC_TARGETS, artifact.python_dir) for result in artifact.run_clis(): assert result.returncode == 1, result.stdout + result.stderr assert "0 specialization consumers" in result.stdout + result.stderr diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py index 66cda4e86eec..1e8567bd8a1b 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_desk_check.py @@ -1,7 +1,9 @@ """The desk-check invariants of RUNBOOK §4's host boundary, as a real, runnable CLI. - python3 tools/hkp_desk_check.py --mode structural - python3 tools/hkp_desk_check.py --mode full + python3 tools/hkp_desk_check.py --mode structural \\ + --generic-targets-json + python3 tools/hkp_desk_check.py --mode full \\ + --generic-targets-json `--mode structural` reads the descriptors against themselves and each other: drift between metadata and the authored spec, duplicate matcher tuples, toc_key @@ -57,10 +59,12 @@ MODES, DeskCheckReport, compiled_agreement, + load_kdp_arch, load_variant_set, metadata_identity_fields, ) from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_pack.generic_targets import GenericTargets # noqa: E402 from hkp_pack.provenance_sidecar import PACKED_MARKER, is_packed # noqa: E402 @@ -88,6 +92,13 @@ def _parse_args(argv): "reports compiled agreement as NOT CHECKED. Required: a default would " "let a structural run read as a full one.", ) + p.add_argument( + "--generic-targets-json", + required=True, + help="The generic GPU target table (gpu_generic_targets.json). Duplicate " + "matcher tuples are judged per arch tier, and a generic stands for its " + "table members.", + ) p.add_argument( "--kpack-python-dir", default=None, @@ -153,6 +164,11 @@ def main(argv=None): args = _parse_args(sys.argv[1:] if argv is None else argv) drift_fields = tuple(args.drift_fields) if args.drift_fields else None kdp = Path(args.kdp) + try: + generic_targets = GenericTargets.load(args.generic_targets_json) + except HkpPackError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + return 1 roots = { "provenance_root": args.provenance_root, "descriptor_root": args.descriptor_root, @@ -166,7 +182,7 @@ def main(argv=None): # question with no. try: failures, unclaimed, verified = compiled_agreement( - kdp, args.kpack_python_dir, **roots + kdp, generic_targets, args.kpack_python_dir, **roots ) except HkpPackError as exc: failures = [str(exc)] @@ -187,6 +203,8 @@ def main(argv=None): fields, drift_fields, mode=args.mode, + generic_targets=generic_targets, + kdp_arch=load_kdp_arch(kdp), agreement_failures=failures, agreement_unclaimed=unclaimed, agreement_verified=verified, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py index ffe7f8c9716c..282406247f59 100644 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_pack.py @@ -112,6 +112,11 @@ def _parse_args(argv): "scratch inside whatever tree the output is written to, which is now a " "staged, installable directory.", ) + p.add_argument( + "--generic-targets-json", + required=True, + help="The generic GPU target table (gpu_generic_targets.json).", + ) p.add_argument( "--kpack-python-dir", default=None, @@ -171,6 +176,7 @@ def main(argv=None): arches=arches, out_root=Path(args.out_root), hipcc=args.hipcc, + generic_targets_json=Path(args.generic_targets_json), rocm_kpack_dir=args.kpack_python_dir, inter_root=Path(args.inter_root), rocke_wheel_stamp=args.rocke_wheel_stamp, diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py index 1743cb1fdc5a..94d9c9af4eeb 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_assert.py @@ -6,12 +6,16 @@ failure, unless the shipped set is the one `--expect` lists (hkp_probe_derive_root.py writes it) and every UKD is well formed for its kind: a JSON list of {"kdp": , "name": , "kind": -}, plus the authored "kernel_source" for kinds whose -output is a pass-through. The checks applied to a UKD follow its EXPECTED kind's output -type (hkp_probe_kinds.KINDS): `kpack` UKDs are checked against the archive; -`passthrough` UKDs must ship as authored. The archive checks (kpack-missing, -kpack-empty, kpack-toc, sha256, symbol, signature) run only when the expect list holds -at least one kpack-output UKD; a pass-through-only root ships no archive. +, "arch": }, plus the +authored "kernel_source" for kinds whose output is a pass-through. The pass is `--arch`, +or a generic target containing it: the packer compiles a generic once and merges its +descriptors and archive into the member folder, so the UKD ships under `//` +with `arch` []. The checks applied to a UKD follow its EXPECTED kind's output +type (hkp_probe_kinds.KINDS): `kpack` UKDs are checked against the archive of their +pass; `passthrough` UKDs must ship as authored. The archive of a pass +(//kpack/hip_kernel_provider_.kpack) is checked (kpack-missing, +kpack-empty, kpack-toc, sha256, symbol, signature) for each pass that has at least one +kpack-output UKD in the expect list; a pass-through-only root ships no archive. hkp_probe_assert: FAIL : @@ -25,8 +29,8 @@ stamp-missing / does not exist arch-dir-missing // does not exist extra-arch-dir another gfx* directory sits beside // - kpack-missing kpack output expected but //kpack/hip_kernel_provider_ - .kpack is absent + kpack-missing kpack output expected for a pass but //kpack/ + hip_kernel_provider_.kpack is absent kpack-empty that archive has size 0 no-kdp no *.kdp.json under //, or one is not valid JSON provenance-sidecar a KDP's provenance sidecar is missing, unreadable, or does not @@ -37,13 +41,15 @@ ukd-kind a UKD's shipped kernel_source.kind is not what its expected kind's output type requires ("kpack", or the authored kind for a pass-through) - arch-field a KDP `arch` present and not [], or a UKD `arch` absent - or not [] + arch-field a KDP `arch` present and not [], or a UKD `arch` absent + or not [], where is the expected UKD's `arch` (for + a KDP, that of its expected UKDs; --arch when it has none) passthrough-source a pass-through UKD's kernel_source differs from the authored one passthrough-provenance a pass-through UKD's provenance.source_label is empty or its provenance.source_file differs from kernel_source.source_file - kpack-toc the archive is unreadable or has no entry for a UKD's toc_key + kpack-toc an archive is unreadable or has no entry, under its pass, for a + UKD's toc_key sha256 the archive blob's sha256 differs from kernel_source.sha256 signature a UKD's signature is not a non-empty list symbol a UKD's symbol does not appear in its archive blob @@ -62,9 +68,9 @@ import sys from pathlib import Path -from hkp_probe_kinds import KINDS - -# tools/ holds hkp_pack.py, which must never resolve as the `hkp_pack` package. +# Put the package dir ahead of everything, including this script's own dir: tools/ +# also holds a module literally named hkp_pack (hkp_pack.py), which would otherwise +# shadow the hkp_pack package when tools/ is sys.path[0]. _PKG_ROOT = str(Path(__file__).resolve().parent.parent / "python") while _PKG_ROOT in sys.path: sys.path.remove(_PKG_ROOT) @@ -72,6 +78,8 @@ from hkp_pack import provenance_sidecar # noqa: E402 from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_pack.pipeline import _kpack_filename # noqa: E402 +from hkp_probe_kinds import KINDS # noqa: E402 _PREFIX = "hkp_probe_assert" @@ -105,7 +113,7 @@ class _UsageError(Exception): def _load_expect(path): - """Read --expect into {(kdp, name): entry}; entry keeps `kind` and `kernel_source`.""" + """Read --expect into {(kdp, name): entry}; entry keeps its other fields.""" try: entries = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: @@ -115,10 +123,10 @@ def _load_expect(path): expected = {} for entry in entries: if not isinstance(entry, dict) or not all( - isinstance(entry.get(k), str) for k in ("kdp", "name", "kind") + isinstance(entry.get(k), str) for k in ("kdp", "name", "kind", "arch") ): raise _UsageError( - f"--expect {path} entry {entry!r} lacks string kdp, name and kind" + f"--expect {path} entry {entry!r} lacks string kdp, name, kind and arch" ) if _output_of(entry["kind"]) == "passthrough" and not isinstance( entry.get("kernel_source"), dict @@ -177,9 +185,9 @@ def _check_layout(args, failures): return arch_dir -def _check_kpack(args, arch_dir, failures): - """Check 4. Returns the archive path when it exists and is non-empty.""" - kpack_path = arch_dir / "kpack" / f"hip_kernel_provider_{args.arch}.kpack" +def _check_kpack(arch_dir, pass_arch, failures): + """Check 4 for one pass. Returns the archive path when it is non-empty.""" + kpack_path = arch_dir / "kpack" / _kpack_filename(pass_arch) if not kpack_path.is_file(): failures.add("kpack-missing", f"{kpack_path} does not exist") return None @@ -223,7 +231,7 @@ def _check_provenance(args, label, ukd, kind, failures): _PROVENANCE_CHECKS[name](args, label, prov, failures) -def _check_kpack_ukd(args, label, ukd, expected, archive, failures): +def _check_kpack_ukd(args, label, ukd, expected, archives, failures): source = ukd.get("kernel_source") or {} kind = source.get("kind") if kind != "kpack": @@ -234,7 +242,8 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): ) return - if ukd.get("arch") != [args.arch]: + pass_arch = expected["arch"] + if ukd.get("arch") != [pass_arch]: failures.add("arch-field", f"{label} arch is {ukd.get('arch')!r}") signature = source.get("signature") @@ -243,13 +252,14 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): _check_provenance(args, label, ukd, expected["kind"], failures) + archive = archives.get(pass_arch) if archive is None: return toc_key = source.get("toc_key") - blob = archive.get_kernel(toc_key, args.arch) if toc_key else None + blob = archive.get_kernel(toc_key, pass_arch) if toc_key else None if blob is None: failures.add( - "kpack-toc", f"{label} toc_key {toc_key!r} has no {args.arch} archive entry" + "kpack-toc", f"{label} toc_key {toc_key!r} has no {pass_arch} archive entry" ) return digest = hashlib.sha256(blob).hexdigest() @@ -264,7 +274,7 @@ def _check_kpack_ukd(args, label, ukd, expected, archive, failures): failures.add("symbol", f"{label} symbol {symbol!r} not found in archive blob") -def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): +def _check_passthrough_ukd(args, label, ukd, expected, archives, failures): source = ukd.get("kernel_source") or {} kind = source.get("kind") if kind != expected["kind"]: @@ -275,7 +285,7 @@ def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): ) return - if ukd.get("arch") != [args.arch]: + if ukd.get("arch") != [expected["arch"]]: failures.add("arch-field", f"{label} arch is {ukd.get('arch')!r}") if source != expected["kernel_source"]: @@ -305,7 +315,7 @@ def _check_passthrough_ukd(args, label, ukd, expected, archive, failures): _OUTPUT_CHECKS = {"kpack": _check_kpack_ukd, "passthrough": _check_passthrough_ukd} -def _check_ukd(args, kdp_name, ukd, archive, failures): +def _check_ukd(args, kdp_name, ukd, archives, failures): """Check one shipped UKD against its expectation. True when it was expected.""" label = f"{kdp_name}:{ukd.get('name', ukd.get('id', '?'))}" expected = args.expected.get((kdp_name, ukd.get("name"))) @@ -321,11 +331,17 @@ def _check_ukd(args, kdp_name, ukd, archive, failures): f"{label} expected kind {expected['kind']!r} has no rules", ) return True - _OUTPUT_CHECKS[output](args, label, ukd, expected, archive, failures) + _OUTPUT_CHECKS[output](args, label, ukd, expected, archives, failures) return True -def _check_descriptors(args, arch_dir, archive, failures): +def _kdp_arch(args, kdp_name): + """The arch a shipped KDP carries: the pass of its expected UKDs, else --arch.""" + passes = sorted({e["arch"] for (k, _), e in args.expected.items() if k == kdp_name}) + return passes if passes else [args.arch] + + +def _check_descriptors(args, arch_dir, archives, failures): """Checks 5-7: every UKD of every shipped KDP.""" kdps = sorted(arch_dir.rglob("*.kdp.json")) if not kdps: @@ -346,10 +362,10 @@ def _check_descriptors(args, arch_dir, archive, failures): except HkpPackError as exc: failures.add("provenance-sidecar", f"{name}: {exc}") continue - if "arch" in kdp and kdp["arch"] != [args.arch]: + if "arch" in kdp and kdp["arch"] != _kdp_arch(args, name): failures.add("arch-field", f"{name} arch is {kdp['arch']!r}") for ukd in kdp.get("kernelDescriptors", []): - if _check_ukd(args, name, ukd, archive, failures): + if _check_ukd(args, name, ukd, archives, failures): matched += 1 expected = len(args.expected) if matched != expected or matched < 1: @@ -360,9 +376,11 @@ def _check_descriptors(args, arch_dir, archive, failures): ) -def _needs_archive(args): - """Whether any expected UKD has kpack output, so the archive must exist.""" - return any(_output_of(e["kind"]) == "kpack" for e in args.expected.values()) +def _archive_passes(args): + """The passes with an expected UKD of kpack output, whose archive must exist.""" + return sorted( + {e["arch"] for e in args.expected.values() if _output_of(e["kind"]) == "kpack"} + ) def run(args): @@ -374,19 +392,19 @@ def run(args): arch_dir = _check_layout(args, failures) if arch_dir is None: return failures - archive = None - if _needs_archive(args): - kpack_path = _check_kpack(args, arch_dir, failures) + archives = {} + for pass_arch in _archive_passes(args): + kpack_path = _check_kpack(arch_dir, pass_arch, failures) if kpack_path is not None: kpack = _load_kpack(args.kpack_python_dir) try: - archive = kpack.PackedKernelArchive.read(kpack_path) + archives[pass_arch] = kpack.PackedKernelArchive.read(kpack_path) except Exception as exc: failures.add( "kpack-toc", f"{kpack_path} is unreadable: {type(exc).__name__}: {exc}", ) - _check_descriptors(args, arch_dir, archive, failures) + _check_descriptors(args, arch_dir, archives, failures) return failures diff --git a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py index 8c68b7292f5e..e42cdbdc8380 100755 --- a/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py +++ b/dnn-providers/hip-kernel-provider/descriptor-packaging/tools/hkp_probe_derive_root.py @@ -3,12 +3,16 @@ Copies every file under `--from` to `--out`, except that each KDP shipping for `--arch` has its inline `kernelDescriptors` reduced to the UKDs the probe packs, -among the UKDs that themselves ship for `--arch`. +among the UKDs that themselves ship for that pass. -The root is read with the packer's own loader (`hkp_pack.descriptors.load_flat_input`), -so derive sees exactly the descriptors the packer sees and rejects what the packer -rejects. Which KDPs ship for `--arch` is the packer's `kdp_survives`, and which of their -UKDs ship is the packer's `arch_matches`: an empty or absent `arch` list is a wildcard. +The root is read with the packer's own loader (`hkp_pack.descriptors.load_flat_input`) +and the generic target table `--generic-targets-json`, so derive sees exactly the +descriptors the packer sees and rejects what the packer rejects. `--arch m` packs the +passes the packer runs for `--arches m` (`hkp_pack.pipeline.generic_passes`): `m` itself +and every generic `g` with `m` among its members that some KDP ships for. Which KDPs +ship for a pass `p` is the packer's `kdp_survives(kdp, flat, p)`, and which of their UKDs +ship is the packer's `arch_matches`: an empty or absent `arch` list is a wildcard. A KDP +that ships for the generic pass is trimmed like one that ships for `m`. `--exclude-folder` and `--disable-kind` are the packer's `exclude_folders` and `disabled_kinds`: the filters of a build that lacks a producer (rocKE OFF). Derive reads @@ -46,16 +50,28 @@ `expect.json` is written beside `--out` (`/expect.json`): a list of {"kdp": , "name": , "kind": }, one entry per kept UKD, for hkp_probe_assert.py. Entries of kinds -whose packed output is a pass-through also carry the authored "kernel_source". +kernel_source.kind>, "arch": }, +one entry per kept UKD, for hkp_probe_assert.py. Entries of kinds whose packed output is +a pass-through also carry the authored "kernel_source". Files are written only when their content differs and files absent from the derived set are removed, so deriving twice leaves every mtime of the derived root alone and the pack stamp stays fresh across a reconfigure. -`--list-arches --from ` prints, one per line and sorted, the union of the -explicit `arch` entries of every KDP and UKD (inline and standalone) under the root. -Descriptors with no `arch` (wildcards) contribute nothing. +`--list-arches --from --generic-targets-json [--exclude ]...` +prints, one per line and sorted by probe arch, the probe arches of the explicit `arch` +entries of every KDP and UKD (inline and standalone) under the root. Descriptors with no +`arch` (wildcards) contribute nothing. A line is the arch followed by the generics it +stands for, space separated: + + - a concrete or unlisted name prints as itself (`gfx942`; a TheRock family name prints + too, for the caller to skip; a `*-generic` absent from the table exits 2, as the + packer's loader rejects it); + - a generic in the table prints as its first member, in table order, followed by the + generic (`gfx1100 gfx11-generic`), unless any of its members is `--exclude`d (the + build's own targets, whose production pack already compiles the generic); + - one line per probe arch: a member that is also named explicitly, or chosen for two + generics, shares one line (`gfx1100 gfx11-generic`). Standalone UKDs (a `kernelDescriptors` entry that is a bare id) are out of probe scope by policy: a KDP that would be trimmed and references one is refused. @@ -96,6 +112,8 @@ load_flat_input, ) # noqa: E402 from hkp_pack.errors import HkpPackError # noqa: E402 +from hkp_pack.generic_targets import GenericTargets # noqa: E402 +from hkp_pack.pipeline import generic_passes # noqa: E402 from hkp_probe_kinds import KINDS # noqa: E402 _PREFIX = "hkp_probe_derive:" @@ -118,14 +136,15 @@ def _discard(*_args, **_kwargs): pass -def _load(src, filters): - """The packer's view of `src` under `filters` (a (exclude_folders, +def _load(src, table, filters): + """The packer's view of `src` under `table` and `filters` (a (exclude_folders, disabled_kinds) pair, as the pack step gets them); DeriveError when the packer rejects it.""" exclude_folders, disabled_kinds = filters try: return load_flat_input( src, + table, log=_discard, exclude_folders=exclude_folders, disabled_kinds=disabled_kinds, @@ -256,7 +275,7 @@ def _derive_kdp_listed(kdp, rel, arch, wanted): return kept -def _plan(src, arch, ukds=None, filters=((), ())): +def _plan(src, arch, table, ukds=None, filters=((), ())): """Return (files, expect): files maps relative posix path -> source Path or bytes. `ukds` is the list of UKD names to keep, or None for the default selection. @@ -267,7 +286,8 @@ def _plan(src, arch, ukds=None, filters=((), ())): raise DeriveError(f"--ukd lists {dupes} more than once") wanted = set(ukds) - flat = _load(src, filters) + flat = _load(src, table, filters) + passes = [arch] + generic_passes(flat, [arch]) files = {} for p in sorted(src.rglob("*")): if p.is_file(): @@ -277,12 +297,15 @@ def _plan(src, arch, ukds=None, filters=((), ())): kdps = {d.path.relative_to(src).as_posix(): d.doc for d in flat.kdps()} for rel in sorted(kdps): kdp = kdps[rel] - if not kdp_survives(kdp, flat, arch): + # A KDP ships under one pass: the arch lists of the two never overlap, since + # the packer rejects a list holding a generic with one of its members. + packed_as = next((p for p in passes if kdp_survives(kdp, flat, p)), None) + if packed_as is None: continue if ukds is None: - kept = _derive_kdp(kdp, rel, arch) + kept = _derive_kdp(kdp, rel, packed_as) else: - kept = _derive_kdp_listed(kdp, rel, arch, wanted) + kept = _derive_kdp_listed(kdp, rel, packed_as, wanted) if not kept: # Only listed UKDs are packed, so this KDP must not ship in full. del files[rel] @@ -290,7 +313,12 @@ def _plan(src, arch, ukds=None, filters=((), ())): kdp = dict(kdp, kernelDescriptors=kept) files[rel] = (json.dumps(kdp, indent=2) + "\n").encode("utf-8") for u in kept: - entry = {"kdp": rel, "name": u["name"], "kind": u["kernel_source"]["kind"]} + entry = { + "kdp": rel, + "name": u["name"], + "kind": u["kernel_source"]["kind"], + "arch": packed_as, + } if KINDS[entry["kind"]].output == "passthrough": # The packer ships the block as authored; the assertion compares it. entry["kernel_source"] = u["kernel_source"] @@ -302,7 +330,7 @@ def _plan(src, arch, ukds=None, filters=((), ())): missing = sorted(wanted - {e["name"] for e in expect}) if ukds else () if missing or (ukds is None and not expect): try: - _, unfiltered = _plan(src, arch, ukds) + _, unfiltered = _plan(src, arch, table, ukds) except DeriveError: unfiltered = [] unfiltered_names = {e["name"] for e in unfiltered} @@ -326,12 +354,8 @@ def _plan(src, arch, ukds=None, filters=((), ())): return files, expect -def list_arches(src: Path, filters=((), ())) -> list[str]: - """Sorted explicit `arch` entries of every KDP and UKD under `src` that survive - `filters` and that some KDP actually ships for (the packer's `kdp_survives`): an - arch only a disabled-kind UKD or an excluded folder carries is not listed, so a - probe of a listed arch always has content to pack.""" - flat = _load(src, filters) +def _named_arches(flat): + """The union of the explicit `arch` entries of every KDP and UKD in `flat`.""" arches = set() for kdp in flat.kdps(): arches.update(kdp.doc.get("arch") or []) @@ -340,8 +364,30 @@ def list_arches(src: Path, filters=((), ())) -> list[str]: arches.update(entry.get("arch") or []) for ukd in flat.ukds(): arches.update(ukd.doc.get("arch") or []) + return arches + + +def list_arches(src: Path, table, excluded, filters=((), ())) -> list[str]: + """The probe arch lines for `src`; see the module docstring for the format. + + Only names some KDP actually ships for under `filters` are considered (the packer's + `kdp_survives`): an arch only a disabled-kind UKD or an excluded folder carries is + not listed, so a probe of a listed arch always has content to pack. + """ + flat = _load(src, table, filters) kdps = [kdp.doc for kdp in flat.kdps()] - return sorted(a for a in arches if any(kdp_survives(k, flat, a) for k in kdps)) + named = [ + a for a in _named_arches(flat) if any(kdp_survives(k, flat, a) for k in kdps) + ] + probes = {} + for name in named: + if not table.has(name): + probes.setdefault(name, set()) + else: + members = table.members(name) + if members and not any(m in excluded for m in members): + probes.setdefault(members[0], set()).add(name) + return [" ".join([arch, *sorted(probes[arch])]) for arch in sorted(probes)] def _same(dest, content): @@ -386,6 +432,7 @@ def derive_root( src: Path, arch: str, out: Path, + table, ukds: list[str] | None = None, filters=((), ()), ) -> tuple[int, str] | None: @@ -396,7 +443,7 @@ def derive_root( if not src.is_dir(): return EXIT_ERROR, f"{_PREFIX} {src} is not a directory" try: - files, expect = _plan(src, arch, ukds, filters) + files, expect = _plan(src, arch, table, ukds, filters) except FilteredOut as exc: return EXIT_FILTERED, f"{_PREFIX} {exc}" except DeriveError as exc: @@ -416,10 +463,25 @@ def derive_root( def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--from", dest="src", type=Path, required=True) + parser.add_argument( + "--generic-targets-json", + type=Path, + required=True, + help="the generic GPU target table (gpu_generic_targets.json)", + ) parser.add_argument( "--list-arches", action="store_true", - help="print the explicit arches of the root's KDPs and UKDs, one per line", + help="print the probe arches of the root's KDPs and UKDs, one line each", + ) + parser.add_argument( + "--exclude", + dest="excluded", + action="append", + default=[], + metavar="ARCH", + help="with --list-arches: a build target (repeatable); a generic with a " + "member among them gets no probe arch", ) parser.add_argument( "--exclude-folder", @@ -450,16 +512,25 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) filters = (tuple(args.exclude_folder), tuple(args.disable_kind)) + if args.excluded and not args.list_arches: + parser.error("--exclude requires --list-arches") + try: + table = GenericTargets.load(args.generic_targets_json) + except HkpPackError as exc: + print(f"{_PREFIX} {exc}", file=sys.stderr) + return 2 + if args.list_arches: if args.arch or args.out or args.ukds: parser.error( - "--list-arches takes only --from, --exclude-folder and --disable-kind" + "--list-arches takes only --from, --generic-targets-json, --exclude, " + "--exclude-folder and --disable-kind" ) if not args.src.is_dir(): print(f"{_PREFIX} {args.src} is not a directory", file=sys.stderr) return 2 try: - arches = list_arches(args.src, filters) + arches = list_arches(args.src, table, set(args.excluded), filters) except DeriveError as exc: print(f"{_PREFIX} {exc}", file=sys.stderr) return 2 @@ -469,7 +540,7 @@ def main(argv: list[str] | None = None) -> int: if not args.arch or args.out is None: parser.error("--arch and --out are required unless --list-arches is given") - error = derive_root(args.src, args.arch, args.out, args.ukds, filters) + error = derive_root(args.src, args.arch, args.out, table, args.ukds, filters) if error is not None: code, message = error print(message, file=sys.stderr) diff --git a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp index f40a884e6a20..5a4925d41a21 100644 --- a/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp +++ b/dnn-providers/hip-kernel-provider/src/compilation/KpackModuleCache.hpp @@ -15,14 +15,18 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include +#include +#include namespace hip_kernel_provider::compilation { @@ -271,8 +275,8 @@ class KpackModuleCache : public ModuleCache& archiveArches, + std::string_view deviceArch) + { + const std::string* best = nullptr; + std::optional bestTier; + for(const auto& candidate : archiveArches) + { + const auto tier = hipdnn_plugin_sdk::entryTier(candidate, deviceArch); + if(tier && (!bestTier || *tier < *bestTier)) + { + best = &candidate; + bestTier = tier; + } + } + if(bestTier == hipdnn_plugin_sdk::ArchTier::GENERIC) + { + HIPDNN_PLUGIN_LOG_INFO("kpack: device arch '" + << deviceArch << "' is served by generic archive key '" << *best + << "'"); + } + return best; + } + /// @throws KpackModuleLoadFailure on any stage that fails. Never returns a null /// module: ModuleCache would decline to cache it, but with no message, and /// the caller could not tell which stage gave up. @@ -306,16 +335,7 @@ class KpackModuleCache : public ModuleCache +#include +#include +#include + +#include + +#include + +#include "PackedKernelSource.hpp" +#include "TestDescriptorRoot.hpp" +#include "compilation/KpackArchive.hpp" +#include "compilation/KpackModuleCache.hpp" + +namespace hip_kernel_provider::compilation +{ +namespace +{ + +using hip_kernel_provider::testing::PackedKernelSource; +using hip_kernel_provider::testing::readPackedKernelSource; +using hip_kernel_provider::testing::unitKpackRoot; + +constexpr const char* GENERIC = "gfx11-generic"; +constexpr const char* GENERIC_KDP = "conv_fwd_generic.kdp.json"; + +std::vector archivesOf(const std::filesystem::path& kpackFile) +{ + KpackArchive archive; + KpackError error; + std::vector arches; + EXPECT_TRUE(archive.open(kpackFile, error)) << kpackFile << " (" << error.codeName << ")"; + EXPECT_TRUE(archive.architectures(arches, error)) << kpackFile << " (" << error.codeName << ")"; + return arches; +} + +/// Packed arch shards in this tree, enumerated from the filesystem (no device needed). +std::vector packedShards() +{ + std::vector shards; + std::error_code ec; + for(const auto& entry : std::filesystem::directory_iterator(unitKpackRoot(), ec)) + { + if(entry.is_directory(ec) && std::filesystem::is_directory(entry.path() / "kpack", ec)) + { + shards.push_back(entry.path()); + } + } + std::sort(shards.begin(), shards.end()); + return shards; +} + +} // namespace + +TEST(TestPackedGenericArchive, ShardsCarryTheGenericArchiveExactlyWhenTheirArchIsAMember) +{ + const auto shards = packedShards(); + ASSERT_FALSE(shards.empty()) << "no packed arch shard under " << unitKpackRoot(); + + for(const auto& shard : shards) + { + const std::string arch = shard.filename().string(); + SCOPED_TRACE(arch); + + std::vector kpackFiles; + for(const auto& entry : std::filesystem::directory_iterator(shard / "kpack")) + { + kpackFiles.push_back(entry.path()); + } + ASSERT_FALSE(kpackFiles.empty()); + + const bool member = hipdnn_plugin_sdk::genericTargetContains(GENERIC, arch); + + if(!member) + { + for(const auto& file : kpackFiles) + { + const auto arches = archivesOf(file); + EXPECT_EQ(std::count(arches.begin(), arches.end(), GENERIC), 0) << file; + } + EXPECT_FALSE(std::filesystem::exists(shard / "conv_fwd" / GENERIC_KDP)); + continue; + } + + PackedKernelSource packed; + ASSERT_NO_FATAL_FAILURE(readPackedKernelSource(shard, GENERIC_KDP, packed)); + + const std::vector keys = archivesOf(packed.archive); + ASSERT_EQ(keys, std::vector{GENERIC}); + + const std::string* selected = KpackModuleCache::selectArch(keys, arch); + ASSERT_NE(selected, nullptr); + EXPECT_EQ(*selected, GENERIC); + + std::vector both; + for(const auto& file : kpackFiles) + { + for(const auto& key : archivesOf(file)) + { + both.push_back(key); + } + } + ASSERT_NE(std::count(both.begin(), both.end(), arch), 0); + const std::string* preferred = KpackModuleCache::selectArch(both, arch); + ASSERT_NE(preferred, nullptr); + EXPECT_EQ(*preferred, arch); + + KpackArchive archive; + KpackError error; + ASSERT_TRUE(archive.open(packed.archive, error)) << error.codeName; + KpackCodeObject object; + ASSERT_TRUE(archive.codeObject(packed.tocKey, *selected, object, error)) << error.codeName; + EXPECT_FALSE(object.empty()); + } +} + +} // namespace hip_kernel_provider::compilation + +#endif // HIPDNN_ENABLE_KERNEL_INGESTOR diff --git a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp index 2f14b22eb9e8..3313a6f360b4 100644 --- a/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp +++ b/dnn-providers/hip-kernel-provider/src/tests/compilation/TestKpackModuleCache.cpp @@ -278,6 +278,27 @@ TEST(TestKpackModuleCacheLoad, ReportsAnArchTheArchiveDoesNotHold) } } +TEST(TestKpackModuleCacheSelectArch, PrefersTheExactDeviceArchOverAGenericKey) +{ + // The generic key sorts first; first-match-wins would pick it. + const std::vector keys{"gfx11-generic", "gfx1151"}; + + const auto* selected = KpackModuleCache::selectArch(keys, "gfx1151:sramecc+:xnack-"); + + ASSERT_NE(selected, nullptr); + EXPECT_EQ(selected, &keys[1]); +} + +TEST(TestKpackModuleCacheSelectArch, FallsBackToAGenericKeyContainingTheDevice) +{ + const std::vector keys{"gfx942", "gfx12-generic", "gfx11-generic"}; + + const auto* selected = KpackModuleCache::selectArch(keys, "gfx1100:xnack-"); + + ASSERT_NE(selected, nullptr); + EXPECT_EQ(selected, &keys[2]); +} + TEST(TestKpackModuleCacheLoad, ReportsAnArchiveThatDeclaresNoArchitectures) { const ScopedDirectory scratch = claimScratchDirectory("kpackmodulecache"); diff --git a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md index 1ffb1e59b810..bddcedc1753f 100644 --- a/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md +++ b/projects/hipdnn/docs/rfcs/0017_UniversalKernelDescriptor.md @@ -575,7 +575,9 @@ basis on which to prefer one over the other, and no answer to give. The catalog therefore keyed on the tuple: a duplicate key is logged as an error and the colliding kernel dropped, rather than silently admitted into a set where selection cannot choose between its entries. Uniqueness is engine-wide, not per-pack, because the catalog spans -every KDP that names the engine. +every KDP that names the engine. A device sees at most one kernel per tuple: kernels sharing a +tuple are resolved by arch tier (step 4 of the load flow, below), and only same-tier +duplicates are errors. The remedy when two kernels must coexist is to add the KMD field that distinguishes them, so the schema grows to describe the variants the engine spans. This is additive and @@ -1425,6 +1427,19 @@ provider's descriptor cache, reused by every later graph. contributes a matcher set, one dispatch-descriptor (**UDD**) id, and a kernel vector with each kernel's metadata values. A pack whose `arch` list excludes this device is dropped here. The dispatch descriptor is named but **not** loaded; nothing dispatches yet. + +*Arch tiers.* For one device, each kernel sits in one tier: an explicit arch id (e.g. `gfx1151`) +outranks an LLVM generic target containing the device (e.g. `gfx11-generic`), which outranks an +empty `arch` (unrestricted). When kernels sharing one metadata tuple are admitted for the device, +the loader keeps the one in the best tier, so the tuple stays unique in the catalog. Two kernels +with the same tuple in the *same* tier on a device (two explicit lists naming `gfx1151`, two +generics sharing a member, two empty-`arch` kernels) are an identical definition with no basis to +pick one, and the collision is an error. A family may ship a generic-target +build for portability and a member-specialized build with identical metadata; the specialized build +is expected to be better tuned for its device, so it wins there, and the generic remains the +fallback on the other members. The same ranking picks which arch key of one kernel's archive is +loaded: explicit before generic. + *Stored:* the parsed packs, matchers, and kernel metadata, in the same descriptor cache. **5. Run the engine's `graph_match`, lazily, on the first pack that cleared the gate.** It either diff --git a/projects/hipdnn/plugin_sdk/CMakeLists.txt b/projects/hipdnn/plugin_sdk/CMakeLists.txt index 40136efea51a..46146d57f664 100644 --- a/projects/hipdnn/plugin_sdk/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/CMakeLists.txt @@ -21,6 +21,52 @@ hipdnn_generate_version_header(hipdnn_plugin_sdk) target_link_libraries(hipdnn_plugin_sdk INTERFACE hipdnn_flatbuffers_sdk hipdnn_data_sdk) +# Generic GPU target table: data/gpu_generic_targets.json is the single source of truth. +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ValidateGpuGenericTargets.cmake") +set(HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/data/gpu_generic_targets.json") +hipdnn_validate_gpu_generic_targets("${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}") +set_property( + DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" +) + +file(READ "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" _generic_targets_json) +string(JSON _generic_count LENGTH "${_generic_targets_json}" generics) +set(HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS "") +set(HIPDNN_GENERIC_TARGET_ROWS "") +set(HIPDNN_GENERIC_TARGET_ROW_COUNT ${_generic_count}) +math(EXPR _last_generic "${_generic_count} - 1") +# cmake-lint: disable=E1120 +foreach(_g RANGE 0 ${_last_generic}) + string(JSON _generic MEMBER "${_generic_targets_json}" generics ${_g}) + string(TOUPPER "${_generic}" _array_name) + string(REPLACE "-" "_" _array_name "${_array_name}") + string(JSON _member_count LENGTH "${_generic_targets_json}" generics "${_generic}") + math(EXPR _last_member "${_member_count} - 1") + set(_member_literals "") + foreach(_m RANGE 0 ${_last_member}) + string(JSON _member GET "${_generic_targets_json}" generics "${_generic}" ${_m}) + string(APPEND _member_literals " \"${_member}\",\n") + endforeach() + string(APPEND HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS + "inline constexpr std::string_view ${_array_name}_MEMBERS[] = {\n${_member_literals}};\n\n") + string(APPEND HIPDNN_GENERIC_TARGET_ROWS + " {\"${_generic}\", ${_array_name}_MEMBERS, ${_member_count}},\n") +endforeach() +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GpuGenericTargetsTable.hpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/include/hipdnn_plugin_sdk/GpuGenericTargetsTable.hpp" @ONLY +) + +# Beside the build-tree Config, as the installed copy sits beside the installed Config. +set(_generic_targets_build_copy + "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk/gpu_generic_targets.json" +) +configure_file("${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" "${_generic_targets_build_copy}" COPYONLY) +# CACHE INTERNAL so sibling subdirectories see it. +set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "${_generic_targets_build_copy}" + CACHE INTERNAL "Path to the generic GPU target table JSON shipped with the plugin SDK" +) + # The ingestor headers are wholly inside `#ifdef HIPDNN_ENABLE_KERNEL_INGESTOR`, and they # are installed whenever it is set. Carrying the definition on the exported target is what # makes them usable: without it a consumer that find_package()s this SDK gets the headers @@ -91,6 +137,18 @@ install( COMPONENT Development ) +install( + FILES "${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE}" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk + COMPONENT Development +) + +install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/include/hipdnn_plugin_sdk/GpuGenericTargetsTable.hpp" + DESTINATION ${HIP_DNN_PLUGIN_SDK_INSTALL_INCLUDE_DIR}/hipdnn_plugin_sdk + COMPONENT Development +) + install(EXPORT hipdnn_plugin_sdk_targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hipdnn_plugin_sdk FILE hipdnn_plugin_sdkTargets.cmake COMPONENT Development ) diff --git a/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in new file mode 100644 index 000000000000..bf8e8524decc --- /dev/null +++ b/projects/hipdnn/plugin_sdk/cmake/GpuGenericTargetsTable.hpp.in @@ -0,0 +1,32 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +// Generated by CMake from data/gpu_generic_targets.json. Do not edit. + +#pragma once + +#include +#include + +namespace hipdnn_plugin_sdk::generated +{ + +/// One LLVM generic processor and its member processors. +struct GenericTargetRow +{ + std::string_view name; + const std::string_view* members; + std::size_t memberCount; +}; + +// clang-format off +// NOLINTBEGIN(modernize-avoid-c-arrays) +@HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS@ +inline constexpr GenericTargetRow GENERIC_TARGET_ROWS[] = { +@HIPDNN_GENERIC_TARGET_ROWS@}; +// NOLINTEND(modernize-avoid-c-arrays) +// clang-format on + +inline constexpr std::size_t GENERIC_TARGET_ROW_COUNT = @HIPDNN_GENERIC_TARGET_ROW_COUNT@; + +} // namespace hipdnn_plugin_sdk::generated diff --git a/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake new file mode 100644 index 000000000000..ef9fe4d50406 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/cmake/ValidateGpuGenericTargets.cmake @@ -0,0 +1,75 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +# Validates data/gpu_generic_targets.json; each violation is a FATAL_ERROR naming the key. +# cmake-lint: disable=E1120 + +# Checks the generics map: names, member shape, duplicates. +function(_hipdnn_gpu_generic_targets_validate_generics json json_path) + string(JSON generics_type ERROR_VARIABLE err TYPE "${json}" generics) + if(err OR NOT generics_type STREQUAL "OBJECT") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required object 'generics'") + endif() + string(JSON generic_count LENGTH "${json}" generics) + if(generic_count EQUAL 0) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'generics' must be non-empty") + endif() + + math(EXPR last_generic "${generic_count} - 1") + foreach(g RANGE 0 ${last_generic}) + string(JSON generic MEMBER "${json}" generics ${g}) + if(NOT generic MATCHES "^gfx[0-9]+(-[0-9]+)?-generic$") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generic name '${generic}' does not match ^gfx[0-9]+(-[0-9]+)?-generic$") + endif() + string(JSON members_type TYPE "${json}" generics "${generic}") + if(NOT members_type STREQUAL "ARRAY") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generics.${generic} must be an array") + endif() + string(JSON member_count LENGTH "${json}" generics "${generic}") + if(member_count EQUAL 0) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generics.${generic} must list at least one member") + endif() + + set(seen "") + math(EXPR last_member "${member_count} - 1") + foreach(m RANGE 0 ${last_member}) + string(JSON member_type TYPE "${json}" generics "${generic}" ${m}) + if(NOT member_type STREQUAL "STRING") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: generics.${generic}[${m}] must be a string") + endif() + string(JSON member GET "${json}" generics "${generic}" ${m}) + if(NOT member MATCHES "^gfx[0-9a-f]+$") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: member '${member}' of ${generic} does not match ^gfx[0-9a-f]+$") + endif() + list(FIND seen "${member}" seen_at) + if(NOT seen_at EQUAL -1) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: member '${member}' is listed twice in ${generic}") + endif() + list(APPEND seen "${member}") + endforeach() + endforeach() +endfunction() + +# Validates the schema version and every generic; FATAL_ERROR on the first violation. +function(hipdnn_validate_gpu_generic_targets json_path) + if(NOT EXISTS "${json_path}") + message(FATAL_ERROR "gpu_generic_targets: cannot read '${json_path}'") + endif() + file(READ "${json_path}" json) + + string(JSON _root_type ERROR_VARIABLE parse_error TYPE "${json}") + if(parse_error) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: not valid JSON: ${parse_error}") + endif() + + string(JSON version ERROR_VARIABLE err GET "${json}" schemaVersion) + if(err) + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: missing required key 'schemaVersion'") + endif() + string(JSON version_type TYPE "${json}" schemaVersion) + if(NOT version_type STREQUAL "NUMBER" OR NOT version STREQUAL "1") + message(FATAL_ERROR "gpu_generic_targets: ${json_path}: 'schemaVersion' must be 1, got '${version}'") + endif() + + _hipdnn_gpu_generic_targets_validate_generics("${json}" "${json_path}") +endfunction() diff --git a/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in b/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in index 30bcd5e07d56..88ab2a62399f 100644 --- a/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in +++ b/projects/hipdnn/plugin_sdk/cmake/hipdnn_plugin_sdkConfig.cmake.in @@ -15,6 +15,8 @@ if(HIPDNN_PLUGIN_SDK_ENABLE_KERNEL_INGESTOR) find_dependency(nlohmann_json REQUIRED) endif() +set(HIPDNN_PLUGIN_SDK_GPU_GENERIC_TARGETS_JSON "${CMAKE_CURRENT_LIST_DIR}/gpu_generic_targets.json") + include("${CMAKE_CURRENT_LIST_DIR}/hipdnn_plugin_sdkTargets.cmake") check_required_components(hipdnn_plugin_sdk) diff --git a/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json new file mode 100644 index 000000000000..0fd253f3a7d4 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/data/gpu_generic_targets.json @@ -0,0 +1,13 @@ +{ + "$comment": "Membership of LLVM AMDGPU generic processors; the C++ constexpr table is generated from this file at CMake time and the packer reads it at pack time. Edit this file, never a copy.", + "schemaVersion": 1, + "source": { + "document": "llvm/docs/AMDGPUUsage.rst, 'AMDGPU Generic Processors' table", + "revision": "https://github.com/ROCm/llvm-project/blob/998a696feca902c689c53383e48d29494f9edba7/llvm/docs/AMDGPUUsage.rst#L606-L711", + "pinnedBy": "compiler/amd-llvm submodule of ROCm/TheRock main at 4bed01044 (the compiler TheRock builds hipDNN with); packer exercised with TheRock e8958f6709fa (run 37549184513) compiler: AMD clang version 24.0.0git (https://github.com/ROCm/llvm-project.git 7667f52dd687899256a3285ab0bce3c53bc73394)" + }, + "generics": { + "gfx11-generic": ["gfx1100", "gfx1101", "gfx1102", "gfx1103", "gfx1150", "gfx1151", "gfx1152", "gfx1153"], + "gfx12-generic": ["gfx1200", "gfx1201"] + } +} diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp new file mode 100644 index 000000000000..e64f045e8318 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/GpuGenericTargets.hpp @@ -0,0 +1,81 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +#pragma once + +/// LLVM generic GPU targets (`gfx11-generic`, ...) and how an `arch` entry ranks for a device. +/// Membership comes only from the generated table, never from a name's shape. + +#include +#include +#include + +#include +#include + +namespace hipdnn_plugin_sdk +{ + +/// Shape only (`...-generic`); use findGenericTarget for membership. +inline bool isGenericShapedArchName(std::string_view name) +{ + constexpr std::string_view SUFFIX = "-generic"; + return name.size() >= SUFFIX.size() + && name.compare(name.size() - SUFFIX.size(), std::string_view::npos, SUFFIX) == 0; +} + +/// The table row for @p name, or nullptr. +inline const generated::GenericTargetRow* findGenericTarget(std::string_view name) +{ + for(const auto& row : generated::GENERIC_TARGET_ROWS) + { + if(row.name == name) + { + return &row; + } + } + return nullptr; +} + +/// Is @p baseDeviceId (features stripped) a member of @p generic? False if @p generic is unknown. +inline bool genericTargetContains(std::string_view generic, std::string_view baseDeviceId) +{ + const auto* row = findGenericTarget(generic); + if(row == nullptr) + { + return false; + } + for(std::size_t i = 0; i < row->memberCount; ++i) + { + if(row->members[i] == baseDeviceId) + { + return true; + } + } + return false; +} + +/// How specifically an `arch` entry names a device; a lower value wins. +enum class ArchTier : int +{ + EXPLICIT = 0, ///< The entry is the device's own base id. + GENERIC = 1, ///< A table generic whose members contain the device. + UNRESTRICTED = 2, ///< An empty list: any device. +}; + +/// Rank of @p entry for @p rawDeviceArch (features included); nullopt when it does not +/// admit the device. A generic-shaped entry is never EXPLICIT. +inline std::optional entryTier(std::string_view entry, std::string_view rawDeviceArch) +{ + if(isGenericShapedArchName(entry)) + { + return genericTargetContains(entry, stripArchFeatures(rawDeviceArch)) + ? std::optional(ArchTier::GENERIC) + : std::nullopt; + } + return archMatches(rawDeviceArch, entry, ArchMatchMode::PREFIX) + ? std::optional(ArchTier::EXPLICIT) + : std::nullopt; +} + +} // namespace hipdnn_plugin_sdk diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp index a2faa8ba22f2..ec00835d1ffb 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DescriptorLoader.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -669,10 +670,12 @@ inline void requireNoDuplicates(const std::vector& values, /// reports features too (`gfx942:sramecc+:xnack-`) and archMatches stops the candidate /// at ':', so a bare id matches such a device -- but an authored entry may not carry /// features. Matching compares target-id text, so a partial id like `gfx942:xnack-` -/// reads as reasonable and matches nothing. LLVM generic targets (`gfx9-4-generic`) stay -/// legal: the '-' is part of the base id. Catches an authoring typo, which would +/// reads as reasonable and matches nothing. LLVM generic targets (`gfx9-4-generic`) are +/// well-formed: the '-' is part of the base id. Catches an authoring typo, which would /// otherwise disable the pack everywhere with nothing louder than an INFO decline; not -/// an existence check, so an unheard-of but well-formed id still parses. +/// an existence check on its own, so an unheard-of but well-formed id still passes here. +/// A generic name absent from the generic target table is rejected separately, by +/// requireArchList. inline bool isPlausibleArchBaseId(std::string_view value) { constexpr std::string_view PREFIX = "gfx"; @@ -704,6 +707,74 @@ inline std::string describeArch(const std::vector& arch) return text + "]"; } +/// A device both table entries @p x and @p y cover, or empty. +inline std::string sharedGenericMember(const std::string& x, const std::string& y) +{ + const auto* generic = findGenericTarget(x); + const std::string& other = generic != nullptr ? y : x; + if(generic == nullptr) + { + generic = findGenericTarget(y); + } + if(generic == nullptr) + { + return {}; + } + for(size_t i = 0; i < generic->memberCount; ++i) + { + if(entryTier(other, generic->members[i]).has_value()) + { + return std::string(generic->members[i]); + } + } + return {}; +} + +/// Why @p kernelArch breaks the generic rules against its pack's @p packArch, or empty: a +/// kernel with its own arch must list every pack generic and only entries the pack lists. +inline std::string genericArchViolation(const std::vector& packArch, + const std::vector& kernelArch) +{ + const auto lists = [](const std::vector& arch, const std::string& entry) { + return std::find(arch.begin(), arch.end(), entry) != arch.end(); + }; + for(const auto& entry : kernelArch) + { + if(isGenericShapedArchName(entry) && !lists(packArch, entry)) + { + return "declares generic target '" + entry + "', which its pack does not list"; + } + } + if(kernelArch.empty()) + { + return {}; + } + bool packHasGeneric = false; + for(const auto& entry : packArch) + { + if(isGenericShapedArchName(entry)) + { + packHasGeneric = true; + if(!lists(kernelArch, entry)) + { + return "declares arch " + describeArch(kernelArch) + + " without the pack's generic target '" + entry + "'"; + } + } + } + if(packHasGeneric) + { + for(const auto& entry : kernelArch) + { + if(!lists(packArch, entry)) + { + return "declares arch '" + entry + "', which its generic-target pack does not list"; + } + } + } + return {}; +} + /// `arch`: every entry must be non-empty, non-repeated, and a plausible gfx base id. /// archSupports is a case-sensitive exact compare, so `""`, `" gfx942"`, or `"gfx94"` /// would otherwise silently disable the pack everywhere with nothing louder than an @@ -733,6 +804,36 @@ inline std::vector requireArchList(const nlohmann::json& object, } } requireNoDuplicates(values, "arch entry", where); + for(const auto& value : values) + { + if(isGenericShapedArchName(value) && findGenericTarget(value) == nullptr) + { + std::string message = "key 'arch' in "; + message += where; + message += " names '"; + message += value; + message += "', a generic target absent from the generic target table"; + fail(message); + } + } + for(size_t i = 0; i < values.size(); ++i) + { + for(size_t j = i + 1; j < values.size(); ++j) + { + if(const auto shared = sharedGenericMember(values[i], values[j]); !shared.empty()) + { + std::string message = "key 'arch' in "; + message += where; + message += " lists '"; + message += values[i]; + message += "' and '"; + message += values[j]; + message += "', which both cover "; + message += shared; + fail(message); + } + } + } return values; } @@ -1109,6 +1210,16 @@ inline void addPackKernelEntry(KernelDescriptorPack& pack, + describeArch(kernel->arch) + ", which reaches past the pack's " + describeArch(pack.arch)); } + if(const auto violation = genericArchViolation(pack.arch, kernel->arch); !violation.empty()) + { + std::string message = "kernel '"; + message += kernel->name; + message += "' in "; + message += where; + message += " "; + message += violation; + fail(message); + } pack.kernels.push_back(std::move(*kernel)); } @@ -2145,9 +2256,9 @@ inline std::vector resolveDescriptorSets(DescriptorCatalog catalo { // Failure granularity is the pack: a pack whose cross-references dangle or // whose kernels contradict the KMD is dropped while the engine keeps its other - // packs. A duplicate kernel metadata tuple is the exception, for packs whose - // arch lists overlap -- the state manager's constructor throws on it, taking - // the whole engine. RFC 0017 §10 + // packs. A duplicate kernel metadata tuple is the exception, for kernels that + // compete at the same arch tier -- the state manager's constructor throws on it, + // taking the whole engine. RFC 0017 §10 // wants only the colliding kernel dropped; the upgrade is making that // constructor log and drop rather than throw, in one place, so hand-built packs // get the same behavior. @@ -2209,6 +2320,13 @@ inline std::vector resolveDescriptorSets(DescriptorCatalog catalo reason = match.reason; break; } + if(const auto violation + = detail::genericArchViolation(pack.arch, match.kernel->arch); + !violation.empty()) + { + reason = "names kernel " + toString(kernelId) + ", which " + violation; + break; + } pack.kernels.push_back(*match.kernel); pack.kernels.back().originDirectory = match.path.parent_path(); pack.kernels.back().treeRoot = match.treeRoot; diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp index 0cae60704112..337da22ee861 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/Descriptors.hpp @@ -105,7 +105,8 @@ static_assert( std::vector>, "MetadataType::INT_LIST no longer indexes MetadataValue's vector alternative."); -/// A kernel's complete metadata tuple; must be unique per kernel within an engine. +/// A kernel's complete metadata tuple. Kernels of one engine sharing a tuple must not compete at +/// the same arch tier on any device; a better tier shadows a worse one. using MetadataValues = std::map; /// One field a kernel may vary along, as declared by an engine's KMD. @@ -379,8 +380,9 @@ struct KernelDescriptorPack std::vector matcherIds; DescriptorId engineId; DescriptorId dispatchId; - /// GFX targets, e.g. `{"gfx942", "gfx950"}`; empty means arch-independent, matched - /// exactly against the device's base target id. Part of the pack's catalog identity + /// GFX targets, e.g. `{"gfx942", "gfx950"}`; empty means arch-independent. A concrete + /// entry is matched exactly against the device's base target id; a generic entry + /// matches by membership in the generic target table. Part of the pack's catalog identity /// -- packs are keyed by (id, arch), so per-arch shards may ship one pack id many /// times -- as well as a match-time filter. The filter itself is enforced at catalog /// build, not load time, so an excluded pack still builds and simply declines per diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp index 26bb349bcd3a..aa0aa8a8917a 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/DeviceProperties.hpp @@ -6,11 +6,14 @@ #ifdef HIPDNN_ENABLE_KERNEL_INGESTOR #include +#include +#include #include #include #include #include +#include namespace hipdnn_plugin_sdk::ingestor { @@ -27,39 +30,88 @@ struct DeviceProperties int multiProcessorCount = 0; ///< Compute units; 0 if unresolved. }; -/// Does @p arch (a KDP's supported-target list; empty admits everything) admit -/// @p deviceArch? Entries are base ids and the device carries its features, so this is -/// the PREFIX match, not SUBSTRING or equality: `gfx942` admits a device reporting -/// `gfx942:sramecc+:xnack-` and never admits `gfx950`. +/// Best entryTier of any entry of @p arch for @p deviceArch; empty list is UNRESTRICTED. +inline std::optional archTier(const std::vector& arch, + std::string_view deviceArch) +{ + if(arch.empty()) + { + return ArchTier::UNRESTRICTED; + } + std::optional best; + for(const auto& entry : arch) + { + const auto tier = entryTier(entry, deviceArch); + if(tier && (!best || *tier < *best)) + { + best = tier; + } + } + return best; +} + inline bool archSupports(const std::vector& arch, std::string_view deviceArch) { - return arch.empty() - || std::any_of(arch.begin(), arch.end(), [deviceArch](const std::string& candidate) { - return archMatches(deviceArch, candidate, ArchMatchMode::PREFIX); - }); + return archTier(arch, deviceArch).has_value(); +} + +namespace detail +{ + +inline bool listAdmits(const std::vector& list, std::string_view device) +{ + return std::any_of(list.begin(), list.end(), [device](const std::string& entry) { + return entryTier(entry, device).has_value(); + }); +} + +/// Calls @p visit with each device id @p entry expands to until it returns true. +template +inline bool anyExpandedMember(std::string_view entry, Visit&& visit) +{ + if(!isGenericShapedArchName(entry)) + { + return visit(entry); + } + const auto* row = findGenericTarget(entry); + if(row == nullptr) + { + return false; + } + for(std::size_t i = 0; i < row->memberCount; ++i) + { + if(visit(row->members[i])) + { + return true; + } + } + return false; +} + +inline bool anyExpandedMemberAdmittedBy(const std::vector& from, + const std::vector& into) +{ + return std::any_of(from.begin(), from.end(), [&into](const std::string& entry) { + return anyExpandedMember( + entry, [&into](std::string_view device) { return listAdmits(into, device); }); + }); } -/// Can one device satisfy both @p a and @p b? Empty means "every arch", so it overlaps -/// everything. Both sides are authored base ids, so an entry matches only its twin. +} // namespace detail + +/// Can one device satisfy both @p a and @p b, over generics expanded to members? Empty +/// means every arch; an unknown generic expands to nothing. inline bool archOverlaps(const std::vector& a, const std::vector& b) { if(a.empty() || b.empty()) { return true; } - return std::any_of(a.begin(), a.end(), [&b](const std::string& lhs) { - return std::any_of(b.begin(), b.end(), [&lhs](const std::string& rhs) { - return archMatches(lhs, rhs, ArchMatchMode::PREFIX) - || archMatches(rhs, lhs, ArchMatchMode::PREFIX); - }); - }); + return detail::anyExpandedMemberAdmittedBy(a, b) || detail::anyExpandedMemberAdmittedBy(b, a); } /// Is every device @p inner admits also admitted by @p outer? The asymmetric counterpart -/// to archOverlaps, for asking whether a kernel stays within the pack that binds it. -/// Empty @p outer admits every device, so it covers anything; empty @p inner declares no -/// restriction of its own and is covered by anything. Otherwise every @p inner entry -/// must appear in @p outer: `[gfx942]` is covered by `[gfx942, gfx950]`, not the reverse. +/// to archOverlaps. Empty @p outer covers anything; empty @p inner is covered by anything. inline bool archCovers(const std::vector& outer, const std::vector& inner) { if(outer.empty()) @@ -67,12 +119,32 @@ inline bool archCovers(const std::vector& outer, const std::vector< return true; } return std::all_of(inner.begin(), inner.end(), [&outer](const std::string& entry) { - return std::any_of(outer.begin(), outer.end(), [&entry](const std::string& candidate) { - return archMatches(entry, candidate, ArchMatchMode::PREFIX); + return !detail::anyExpandedMember(entry, [&outer](std::string_view device) { + return !detail::listAdmits(outer, device); }); }); } +/// Do @p a and @p b match some device at the same tier? A better tier shadows a worse one, +/// so lists whose tiers always differ may coexist. Two empty lists compete. +inline bool archesCompete(const std::vector& a, const std::vector& b) +{ + if(a.empty() && b.empty()) + { + return true; + } + const auto tiesOn = [&a, &b](std::string_view device) { + const auto tierA = archTier(a, device); + return tierA && tierA == archTier(b, device); + }; + const auto anyTie = [&tiesOn](const std::vector& list) { + return std::any_of(list.begin(), list.end(), [&tiesOn](const std::string& entry) { + return detail::anyExpandedMember(entry, tiesOn); + }); + }; + return anyTie(a) || anyTie(b); +} + } // namespace hipdnn_plugin_sdk::ingestor #endif // HIPDNN_ENABLE_KERNEL_INGESTOR diff --git a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp index d1bce7e0d9a0..079a0fa1c581 100644 --- a/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp +++ b/projects/hipdnn/plugin_sdk/include/hipdnn_plugin_sdk/ingestor/KernelIngestorStateManager.hpp @@ -139,7 +139,8 @@ class KernelIngestorStateManager /// logs once and changes nothing. static constexpr size_t WINNER_CACHE_WARNING_THRESHOLD = 4096; - /// @throws std::invalid_argument bad pack reference, or duplicate metadata tuple. + /// @throws std::invalid_argument bad pack reference, or two kernels sharing a metadata tuple + /// that compete at the same arch tier on a device. /// @throws std::runtime_error a UMD or the engine's graph_match names a symbol this /// build does not ship. /// @@ -429,11 +430,10 @@ class KernelIngestorStateManager /// than completing each kernel's metadata again on every graph. void validateAndIndexPacks() { - // Two kernels may share a tuple when no single device can see both -- that is - // exactly the per-arch shard layout. Uniqueness is therefore per overlapping-arch - // group, not per engine: the tuple is the catalog key, and a catalog is built for - // one device. Keyed by the tuple's values in a hash map rather than scanned, which - // would be quadratic. + // Two kernels may share a tuple when no single device sees both at the same arch + // tier (per-arch shards, or a more specific kernel that buildCatalog shadows + // per device). Uniqueness is per competing-arch group, keyed by the tuple's values + // in a hash map rather than scanned, which would be quadratic. size_t kernelCount = 0; for(const auto& pack : _packs) { @@ -486,7 +486,8 @@ class KernelIngestorStateManager // one that declared a narrower list claims only that. Claiming by the // kernel rather than the pack is what lets two kernels of ONE pack share a // tuple under disjoint arch -- one implementation per capability -- while - // still catching two that a single device would see together. + // still catching two that compete at the same arch tier on a device. + // Kernels at different tiers do not collide: the better tier wins. std::vector kernelArch = kernel.arch.empty() ? pack.arch : kernel.arch; MetadataTupleValues tuple; tuple.reserve(key.size()); @@ -500,14 +501,14 @@ class KernelIngestorStateManager = archesClaimingTuple.try_emplace(std::move(tuple)).first->second; for(const auto& claimed : claimants) { - if(archOverlaps(claimed, kernelArch)) + if(archesCompete(claimed, kernelArch)) { throw std::invalid_argument( "kernel '" + toString(kernel.id) + "' duplicates the metadata tuple of another kernel under schema '" + _schema.name - + "' on an arch both reach; the tuple is the catalog key " - + "and must be unique per device"); + + "' at the same arch tier on a device both reach; a better tier " + + "shadows a worse one, so equal tiers cannot be told apart"); } } claimants.push_back(kernelArch); @@ -729,12 +730,77 @@ class KernelIngestorStateManager << " kernel(s) after kernel-scoped matching"); } + shadowByArchTier(catalog, context); + HIPDNN_PLUGIN_LOG_INFO("ingestor: catalog for device " << context.deviceId << " holds " << catalog.entries.size() << " kernel(s) from " << _packs.size() << " pack(s)"); return catalog; } + /// Per metadata tuple, keeps only the entries with the best arch tier for the device. + /// Runs after matcher admission, so a declined kernel never hides a lower-tier one. + static void shadowByArchTier(Catalog& catalog, const MatchContext& context) + { + auto& entries = catalog.entries; + if(entries.size() < 2) + { + return; + } + + struct ByMetadata + { + bool operator()(const MetadataValues* lhs, const MetadataValues* rhs) const + { + return *lhs < *rhs; + } + }; + constexpr int SHADOWED = -1; + std::vector tiers; + tiers.reserve(entries.size()); + std::map winners; + for(size_t i = 0; i < entries.size(); ++i) + { + tiers.push_back( + static_cast(archTier(entries[i].arch, context.deviceProperties.gcnArchName) + .value_or(ArchTier::UNRESTRICTED))); + const auto [it, inserted] = winners.try_emplace(&entries[i].metadata, i); + if(!inserted && tiers[i] < tiers[it->second]) + { + it->second = i; + } + } + + bool anyShadowed = false; + for(size_t i = 0; i < entries.size(); ++i) + { + const size_t winner = winners.at(&entries[i].metadata); + if(tiers[i] != tiers[winner]) + { + tiers[i] = SHADOWED; + anyShadowed = true; + HIPDNN_PLUGIN_LOG_INFO("ingestor: kernel " << toString(entries[i].kernelId) + << " is shadowed by kernel " + << toString(entries[winner].kernelId) + << " at a better arch tier on device " + << context.deviceProperties.gcnArchName); + } + } + if(!anyShadowed) + { + return; + } + + // remove_if tests each element in place before moving, so its position indexes tiers. + entries.erase(std::remove_if(entries.begin(), + entries.end(), + [&](const KernelDefinition& entry) { + return tiers[static_cast(&entry - entries.data())] + == SHADOWED; + }), + entries.end()); + } + bool graphLevelMatchersPass(const KernelDescriptorPack& pack, const MatchContext& context, GraphMatcherMemo& graphVerdicts, diff --git a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt index 3d69804dd925..4d30fd5fd4af 100644 --- a/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt +++ b/projects/hipdnn/plugin_sdk/tests/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable(hipdnn_plugin_sdk_tests $<$:ingestor/TestMatchContext.cpp> $<$:ingestor/TestDescriptors.cpp> $<$:ingestor/TestArchGate.cpp> + $<$:ingestor/TestGenericTargets.cpp> $<$:ingestor/TestKernelDefinition.cpp> $<$:ingestor/TestKernelIngestorStateManager.cpp> $<$:ingestor/TestGenericPlan.cpp> @@ -194,3 +195,64 @@ if(HIPDNN_ENABLE_KERNEL_INGESTOR) set_property(GLOBAL APPEND_STRING PROPERTY HIPDNN_INSTALLED_CTEST_STAGING "${_disk_cache_installed_tests}") endif() + +# Validator tests for data/gpu_generic_targets.json; build tree only (not staged into the +# installed CTestTestfile.cmake). PASS_REGULAR_EXPRESSION rather than WILL_FAIL so a crash +# does not pass. CMake wraps messages at spaces, so spaces match any whitespace run. +set(_generic_targets_check "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CheckGpuGenericTargets.cmake") + +add_test( + NAME hipdnn_gpu_generic_targets_accepts_real_table + COMMAND ${CMAKE_COMMAND} -DGENERIC_TARGETS_JSON=${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE} -P + ${_generic_targets_check} +) +set_tests_properties( + hipdnn_gpu_generic_targets_accepts_real_table PROPERTIES PASS_REGULAR_EXPRESSION + "gpu_generic_targets: valid" +) +_apply_hipdnn_test_category_labels(hipdnn_gpu_generic_targets_accepts_real_table) + +# Registers a build-tree test: mutate the real table, expect the validator to reject it with expected_text. +function(_hipdnn_add_generic_targets_rejection_test case op path value expected_text) + string(REPLACE " " "[ \n]+" expected_regex "${expected_text}") + set(test_name hipdnn_gpu_generic_targets_rejects_${case}) + add_test( + NAME ${test_name} + COMMAND + ${CMAKE_COMMAND} -DGENERIC_TARGETS_JSON=${HIPDNN_GPU_GENERIC_TARGETS_JSON_SOURCE} + -DMUTATION_OP=${op} -DMUTATION_PATH=${path} "-DMUTATION_VALUE=${value}" + -DMUTATED_JSON=${CMAKE_CURRENT_BINARY_DIR}/generic_targets_cases/${case}.json -P + ${_generic_targets_check} + ) + set_tests_properties( + ${test_name} + PROPERTIES PASS_REGULAR_EXPRESSION "${expected_regex}" FAIL_REGULAR_EXPRESSION + "gpu_generic_targets: valid" + ) + _apply_hipdnn_test_category_labels(${test_name}) +endfunction() + +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/generic_targets_cases") +_hipdnn_add_generic_targets_rejection_test(not_json RAW "" "{ not json" "not valid JSON") +_hipdnn_add_generic_targets_rejection_test( + wrong_schema_version SET_JSON schemaVersion 2 "'schemaVersion' must be 1, got '2'" +) +_hipdnn_add_generic_targets_rejection_test( + empty_generics SET_JSON generics "{}" "'generics' must be non-empty" +) +_hipdnn_add_generic_targets_rejection_test( + empty_members SET_JSON generics.gfx12-generic "[]" + "generics.gfx12-generic must list at least one member" +) +_hipdnn_add_generic_targets_rejection_test( + duplicate_member SET_JSON generics.gfx12-generic "[\"gfx1200\",\"gfx1200\"]" + "member 'gfx1200' is listed twice in gfx12-generic" +) +_hipdnn_add_generic_targets_rejection_test( + bad_generic_name SET_JSON generics "{\"gfx12generic\":[\"gfx1200\"]}" + "generic name 'gfx12generic' does not match" +) +_hipdnn_add_generic_targets_rejection_test( + bad_member SET_STRING generics.gfx12-generic.0 gfx12zz + "member 'gfx12zz' of gfx12-generic does not match" +) diff --git a/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake new file mode 100644 index 000000000000..a99f5aef79db --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/cmake/CheckGpuGenericTargets.cmake @@ -0,0 +1,36 @@ +# Copyright © Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +# Runs hipdnn_validate_gpu_generic_targets via `cmake -P`, optionally on a mutated copy: +# -DGENERIC_TARGETS_JSON= [-DMUTATION_OP= -DMUTATION_PATH= -DMUTATION_VALUE=] +# MUTATION_OP: SET_STRING, SET_JSON, or RAW (replace the whole document). +# Prints "gpu_generic_targets: valid" only if every rule passed. + +if(NOT GENERIC_TARGETS_JSON) + message(FATAL_ERROR "CheckGpuGenericTargets: pass -DGENERIC_TARGETS_JSON=") +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/ValidateGpuGenericTargets.cmake") + +set(table_path "${GENERIC_TARGETS_JSON}") +if(MUTATION_OP) + if(NOT MUTATED_JSON) + message(FATAL_ERROR "CheckGpuGenericTargets: MUTATION_OP needs -DMUTATED_JSON=") + endif() + file(READ "${GENERIC_TARGETS_JSON}" json) + string(REPLACE "." ";" path "${MUTATION_PATH}") + if(MUTATION_OP STREQUAL "SET_STRING") + string(JSON json SET "${json}" ${path} "\"${MUTATION_VALUE}\"") + elseif(MUTATION_OP STREQUAL "SET_JSON") + string(JSON json SET "${json}" ${path} "${MUTATION_VALUE}") + elseif(MUTATION_OP STREQUAL "RAW") + set(json "${MUTATION_VALUE}") + else() + message(FATAL_ERROR "CheckGpuGenericTargets: unknown MUTATION_OP '${MUTATION_OP}'") + endif() + file(WRITE "${MUTATED_JSON}" "${json}") + set(table_path "${MUTATED_JSON}") +endif() + +hipdnn_validate_gpu_generic_targets("${table_path}") +message(STATUS "gpu_generic_targets: valid") diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp index 59d87c9cdb95..e825b437edd3 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestDescriptorLoader.cpp @@ -2208,24 +2208,111 @@ TEST(TestDescriptorLoader, CarriesAPacksDeclaredArchitectures) EXPECT_EQ(sets.front().packs.front().arch, (std::vector{"gfx90a", "gfx942"})); } -/// The validator must admit exactly what archMatches admits of an authored entry: a bare -/// base id. LLVM generic targets are real gcnArchName values whose base id carries a '-', -/// so a shape check keyed on that character would make them unauthorable. -TEST(TestDescriptorLoader, AcceptsGenericTargetIds) +namespace { - const ScopedSymbols symbols; - const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("pack_arch_generic")); - auto documents = makeSetDocuments('1', "test:arch_generic"); - documentOfType(documents, ".kdp.json")["arch"] - = nlohmann::json::array({"gfx942", "gfx9-4-generic"}); - writeDocuments(dir.path(), documents); - const auto sets = loadFrom(dir.path()); +const std::vector INTACT_ONLY{"test:intact"}; +const std::vector BOTH{"test:candidate", "test:intact"}; - ASSERT_EQ(sets.size(), 1u); - ASSERT_EQ(sets.front().packs.size(), 1u); - EXPECT_EQ(sets.front().packs.front().arch, - (std::vector{"gfx942", "gfx9-4-generic"})); +nlohmann::json arch(std::initializer_list entries) +{ + const nlohmann::json list = std::vector(entries); + return list; +} + +Documents candidateWithKernelArch(const nlohmann::json& packArch, + const nlohmann::json& kernelArch, + bool standalone) +{ + auto documents = makeSetDocuments('2', "test:candidate"); + if(standalone) + { + referenceLastKernel(documents); + documentOfType(documents, ".ukd.json")["arch"] = kernelArch; + documentOfType(documents, ".kdp.json")["arch"] = packArch; + return documents; + } + auto& pack = documentOfType(documents, ".kdp.json"); + pack["arch"] = packArch; + pack.at("kernelDescriptors")[0]["arch"] = kernelArch; + return documents; +} + +/// Sorted names of the engines that load when @p candidate sits beside an intact engine. +std::vector engineNames(const Documents& candidate) +{ + const hipdnn_test_sdk::utilities::ScopedDirectory dir(uniqueDirectory("generic_arch_rule")); + writeDocuments(dir.path(), makeSetDocuments('1', "test:intact")); + writeDocuments(dir.path(), candidate); + + std::vector names; + for(const auto& set : loadFrom(dir.path())) + { + names.push_back(set.engine.name); + } + std::sort(names.begin(), names.end()); + return names; +} + +} // namespace + +/// A generic-shaped name the table does not list matches no device, so its file is dropped. +TEST(TestDescriptorLoader, DropsAPackNamingAGenericAbsentFromTheTable) +{ + EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx99-generic"}), arch({}), false)), + INTACT_ONLY); +} + +TEST(TestDescriptorLoader, DropsAPackListingAGenericAndOneOfItsMembers) +{ + EXPECT_EQ( + engineNames(candidateWithKernelArch(arch({"gfx11-generic", "gfx1151"}), arch({}), false)), + INTACT_ONLY); + EXPECT_EQ( + engineNames(candidateWithKernelArch(arch({"gfx1151", "gfx11-generic"}), arch({}), false)), + INTACT_ONLY); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx12-generic", "gfx942"}), arch({}), false)), + BOTH); +} + +/// A kernel names a generic only when its pack lists that generic itself. +TEST(TestDescriptorLoader, DropsAPackWhoseKernelNamesAGenericThePackDoesNotList) +{ + nlohmann::json members = nlohmann::json::array(); + for(std::size_t i = 0; i < hipdnn_plugin_sdk::findGenericTarget("gfx11-generic")->memberCount; + ++i) + { + members.push_back( + std::string(hipdnn_plugin_sdk::findGenericTarget("gfx11-generic")->members[i])); + } + for(const bool standalone : {false, true}) + { + EXPECT_EQ( + engineNames(candidateWithKernelArch(members, arch({"gfx11-generic"}), standalone)), + INTACT_ONLY) + << (standalone ? "standalone" : "inline"); + } +} + +TEST(TestDescriptorLoader, DropsAPackWhoseKernelOmitsOrExceedsTheGenericPackArch) +{ + for(const bool standalone : {false, true}) + { + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx942"}), arch({"gfx942"}), standalone)), + INTACT_ONLY) + << (standalone ? "standalone" : "inline"); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic"}), arch({"gfx1151"}), standalone)), + INTACT_ONLY) + << (standalone ? "standalone" : "inline"); + EXPECT_EQ(engineNames(candidateWithKernelArch( + arch({"gfx11-generic", "gfx942"}), arch({"gfx11-generic"}), standalone)), + BOTH) + << (standalone ? "standalone" : "inline"); + } + EXPECT_EQ(engineNames(candidateWithKernelArch(arch({"gfx11-generic"}), arch({}), false)), BOTH); } /// The default: a pack naming no architecture applies everywhere, so absence must parse diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp new file mode 100644 index 000000000000..1813a9604a02 --- /dev/null +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestGenericTargets.cpp @@ -0,0 +1,98 @@ +// Copyright © Advanced Micro Devices, Inc., or its affiliates. +// SPDX-License-Identifier: MIT + +#ifdef HIPDNN_ENABLE_KERNEL_INGESTOR + +#include +#include +#include +#include + +#include + +#include +#include + +namespace +{ + +using namespace hipdnn_plugin_sdk; +using namespace hipdnn_plugin_sdk::ingestor; + +using Arch = std::vector; + +TEST(TestGenericTargets, FindsAGenericOnlyByItsExactName) +{ + const auto* row = findGenericTarget("gfx11-generic"); + ASSERT_NE(row, nullptr); + EXPECT_EQ(row->name, "gfx11-generic"); + + EXPECT_EQ(findGenericTarget("gfx11"), nullptr); + EXPECT_EQ(findGenericTarget("gfx9-4-generic"), nullptr); + + EXPECT_TRUE(isGenericShapedArchName("gfx9-4-generic")); + EXPECT_FALSE(isGenericShapedArchName("gfx942")); + EXPECT_FALSE(isGenericShapedArchName("generic")); +} + +TEST(TestGenericTargets, EntryTierRanksExplicitAboveGeneric) +{ + EXPECT_EQ(entryTier("gfx1151", "gfx1151"), ArchTier::EXPLICIT); + EXPECT_EQ(entryTier("gfx1151", "gfx1151:sramecc+:xnack-"), ArchTier::EXPLICIT); + EXPECT_EQ(entryTier("gfx11-generic", "gfx1151"), ArchTier::GENERIC); + EXPECT_EQ(entryTier("gfx11-generic", "gfx1151:sramecc+:xnack-"), ArchTier::GENERIC); + EXPECT_EQ(entryTier("gfx1150", "gfx1151"), std::nullopt); + EXPECT_EQ(entryTier("gfx11-generic", "gfx1154"), std::nullopt); + EXPECT_EQ(entryTier("gfx9-4-generic", "gfx9-4-generic"), std::nullopt); + EXPECT_LT(ArchTier::EXPLICIT, ArchTier::GENERIC); + EXPECT_LT(ArchTier::GENERIC, ArchTier::UNRESTRICTED); +} + +TEST(TestGenericTargets, ListTierIsTheBestOfItsEntries) +{ + const Arch both{"gfx11-generic", "gfx1151"}; + EXPECT_EQ(archTier(both, "gfx1151"), ArchTier::EXPLICIT); + EXPECT_EQ(archTier(both, "gfx1151:sramecc+:xnack-"), ArchTier::EXPLICIT); + EXPECT_EQ(archTier(both, "gfx1100"), ArchTier::GENERIC); + EXPECT_EQ(archTier(both, "gfx1154"), std::nullopt); + EXPECT_EQ(archTier({"gfx11-generic"}, "gfx1151:sramecc+"), ArchTier::GENERIC); +} + +TEST(TestGenericTargets, OverlapIsOverExpandedMemberSets) +{ + EXPECT_TRUE(archOverlaps({"gfx11-generic"}, {"gfx1151"})); + EXPECT_TRUE(archOverlaps({"gfx1151"}, {"gfx11-generic"})); + EXPECT_TRUE(archOverlaps({"gfx11-generic"}, {"gfx11-generic"})); + EXPECT_TRUE(archOverlaps({}, {"gfx11-generic"})); + EXPECT_FALSE(archOverlaps({"gfx11-generic"}, {"gfx12-generic"})); + EXPECT_FALSE(archOverlaps({"gfx11-generic"}, {"gfx1154"})); + EXPECT_FALSE(archOverlaps({"gfx1250"}, {"gfx12-generic"})); +} + +TEST(TestGenericTargets, CoversIsOverExpandedMemberSets) +{ + EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx1151"})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx1100", "gfx1153"})); + EXPECT_FALSE(archCovers({"gfx11-generic"}, {"gfx1154"})); + EXPECT_FALSE(archCovers({"gfx1151"}, {"gfx11-generic"})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, {"gfx11-generic"})); + EXPECT_FALSE(archCovers({"gfx11-generic"}, {"gfx12-generic"})); + EXPECT_TRUE(archCovers({}, {"gfx11-generic"})); + EXPECT_TRUE(archCovers({"gfx11-generic"}, {})); +} + +TEST(TestGenericTargets, CompeteIsSameTierOverlap) +{ + EXPECT_TRUE(archesCompete({}, {})); + EXPECT_TRUE(archesCompete({"gfx942"}, {"gfx942"})); + EXPECT_TRUE(archesCompete({"gfx11-generic"}, {"gfx11-generic"})); + EXPECT_TRUE(archesCompete({"gfx1151", "gfx11-generic"}, {"gfx11-generic"})); + EXPECT_FALSE(archesCompete({}, {"gfx942"})); + EXPECT_FALSE(archesCompete({"gfx11-generic"}, {"gfx1151"})); + EXPECT_FALSE(archesCompete({"gfx11-generic"}, {})); + EXPECT_FALSE(archesCompete({"gfx11-generic"}, {"gfx12-generic"})); +} + +} // namespace + +#endif // HIPDNN_ENABLE_KERNEL_INGESTOR diff --git a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp index fa8a27ac79ac..193ae289a7f8 100644 --- a/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp +++ b/projects/hipdnn/plugin_sdk/tests/ingestor/TestKernelIngestorStateManager.cpp @@ -315,30 +315,162 @@ TEST(TestKernelIngestorStateManager, OffersAnUnstampedKernelEverywhereItsPackRea } } -/// An arch-independent pack claims every device, so it overlaps a per-arch one and the -/// two cannot share a tuple: on a gfx942 device both would apply and the catalog key -/// would name two kernels. This is the empty-list arm of archOverlaps, which the disjoint -/// case above never reaches. -TEST(TestKernelIngestorStateManager, RejectsATupleSharedByAnArchIndependentAndAPerArchPack) +/// One tuple offered at all three tiers keeps only the best per device. Every pack order +/// and an interleaved second query per device (served from the catalog cache) are exercised. +TEST(TestKernelIngestorStateManager, KeepsOnlyTheBestArchTierOfASharedTuplePerDevice) { const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); auto anywhere = makePack({GRAPH_MATCHER_ID}); anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; - auto pinned = makePack({GRAPH_MATCHER_ID}, {"gfx942"}); - pinned.id = testId(0x94); - pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; + auto generic = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + generic.id = testId(0x94); + generic.kernels = {makeKernel(testId(0x95), "kernel_generic", 64, "FLOAT")}; + auto member = makePack({GRAPH_MATCHER_ID}, {"gfx1151"}); + member.id = testId(0x96); + member.kernels = {makeKernel(testId(0x97), "kernel_gfx1151", 64, "FLOAT")}; + + const std::vector> orders{ + {anywhere, generic, member}, {member, generic, anywhere}, {generic, member, anywhere}}; + uint8_t graphIndex = 0x25; + for(const auto& packs : orders) + { + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + packs, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(graphIndex++)); + const auto winnerOn = [&](int deviceId, const char* deviceArch) { + auto properties = testDeviceProperties(); + properties.gcnArchName = deviceArch; + const auto definitions + = manager.unsortedDefinitions(MatchContext{graph, deviceId, properties}); + EXPECT_EQ(definitions.size(), 1U) << deviceArch; + return definitions.empty() ? DescriptorId{} : definitions.front().kernelId; + }; + + for(int round = 0; round < 2; ++round) + { + EXPECT_EQ(winnerOn(0, "gfx1151:sramecc+"), testId(0x97)) << "round " << round; + EXPECT_EQ(winnerOn(1, "gfx1100"), testId(0x95)) << "round " << round; + EXPECT_EQ(winnerOn(2, "gfx942:sramecc+"), testId(0x93)) << "round " << round; + } + } +} + +/// Same generic, same tuple: both rank GENERIC on every member, so the tie is rejected. +TEST(TestKernelIngestorStateManager, RejectsATupleSharedByTwoPacksAtTheSameGenericTier) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + auto first = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + first.kernels = {makeKernel(testId(0x93), "kernel_generic_a", 64, "FLOAT")}; + auto second = makePack({GRAPH_MATCHER_ID}, {"gfx11-generic"}); + second.id = testId(0x94); + second.kernels = {makeKernel(testId(0x95), "kernel_generic_b", 64, "FLOAT")}; EXPECT_THROW(StateManager(makeSchema(), makeTestMatchers(), makeTestDispatches(), - {anywhere, pinned}, + {first, second}, std::make_shared(SCORE_SYMBOL), "test.graph"), std::invalid_argument); } +inline bool declineTheExplicitKernel(const MatchContext& context, + const BoundTokens& bound, + const KernelDefinition& kernel) +{ + return kernel.name != "kernel_gfx942" && acceptFloatKernels(context, bound, kernel); +} + +/// A better-tier kernel a matcher declined must not hide the lower-tier one. +TEST(TestKernelIngestorStateManager, ADeclinedBetterTierKernelDoesNotHideALowerTierOne) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", declineTheExplicitKernel); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + for(const bool explicitFirst : {true, false}) + { + auto anywhere = makePack({GRAPH_MATCHER_ID, KERNEL_MATCHER_ID}); + anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere", 64, "FLOAT")}; + auto pinned = makePack({GRAPH_MATCHER_ID, KERNEL_MATCHER_ID}, {"gfx942"}); + pinned.id = testId(0x94); + pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + explicitFirst ? std::vector{pinned, anywhere} + : std::vector{anywhere, pinned}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x28)); + auto properties = testDeviceProperties(); + properties.gcnArchName = "gfx942:sramecc+"; + const auto definitions = manager.unsortedDefinitions(MatchContext{graph, 0, properties}); + + ASSERT_EQ(definitions.size(), 1U) << "explicitFirst=" << explicitFirst; + EXPECT_EQ(definitions.front().kernelId, testId(0x93)) << "explicitFirst=" << explicitFirst; + } +} + +/// A winner-cache record naming a shadowed kernel cannot resurrect it; it is declined. +TEST(TestKernelIngestorStateManager, AWinnerRecordNamingAShadowedKernelFallsBackToHeuristicOrdering) +{ + const ScopedSymbols symbols("test.graph", acceptGraph, "test.kernel", countingFloatKernels); + const auto criterion = scopedGraphMatcher("test.graph_criterion", &acceptCriterion); + + auto anywhere = makePack({GRAPH_MATCHER_ID}); + anywhere.kernels = {makeKernel(testId(0x93), "kernel_anywhere_64", 64, "FLOAT"), + makeKernel(testId(0x96), "kernel_anywhere_256", 256, "FLOAT")}; + auto pinned = makePack({GRAPH_MATCHER_ID}, {"gfx942"}); + pinned.id = testId(0x94); + pinned.kernels = {makeKernel(testId(0x95), "kernel_gfx942", 64, "FLOAT")}; + + const StateManager manager(makeSchema(), + makeTestMatchers(), + makeTestDispatches(), + {anywhere, pinned}, + std::make_shared(SCORE_SYMBOL), + "test.graph"); + + const TestGraph graph(makeGraphId(0x29)); + auto properties = testDeviceProperties(); + properties.gcnArchName = "gfx942:sramecc+"; + const MatchContext context{graph, 0, properties}; + + const auto baseline = manager.sortedCatalog(context); + ASSERT_EQ(baseline.entries.size(), 2U); + ASSERT_FALSE(baseline.orderedFromRecord); + + const WinnerRecord record{ + RankedEntry{testId(0x93), anywhere.id, DISPATCH_ID, 0.1}, + RankedEntry{testId(0x96), anywhere.id, DISPATCH_ID, 0.2}, + }; + manager.recordWinner( + WinnerKey{hipdnn_flatbuffers_sdk::flatbuffer_utilities::GraphContentKey{graph}, + DeviceKey{properties}}, + record, + WinnerWriteCause::FRESH_MISS); + + const auto after = manager.sortedCatalog(context); + EXPECT_FALSE(after.orderedFromRecord); + ASSERT_EQ(after.entries.size(), baseline.entries.size()); + for(size_t i = 0; i < after.entries.size(); ++i) + { + EXPECT_EQ(after.entries[i].kernelId, baseline.entries[i].kernelId); + EXPECT_NE(after.entries[i].kernelId, testId(0x93)) << "a shadowed kernel was resurrected"; + } +} + TEST(TestKernelIngestorStateManager, APackIsPrunedWhenAnyOfItsCriteriaFails) { // A pack passes only if every criterion it lists passes: the first admits it, the diff --git a/projects/hipdnn/test_categories.yaml b/projects/hipdnn/test_categories.yaml index 77d76223c5a7..13c7c2b6cf27 100644 --- a/projects/hipdnn/test_categories.yaml +++ b/projects/hipdnn/test_categories.yaml @@ -45,6 +45,8 @@ test_categories: - "hipdnn_frontend_tests" - "hipdnn_test_sdk_tests" - "hipdnn_plugin_sdk_tests" + # Build tree only; see plugin_sdk/tests/CMakeLists.txt. + - "hipdnn_gpu_generic_targets_.*" integration: description: "Current hipDNN integration-check coverage" diff --git a/projects/hipdnn/tools/IngestorGenerator/README.md b/projects/hipdnn/tools/IngestorGenerator/README.md index bd081f2ec73b..4afdd54785b5 100644 --- a/projects/hipdnn/tools/IngestorGenerator/README.md +++ b/projects/hipdnn/tools/IngestorGenerator/README.md @@ -296,7 +296,7 @@ machine. | Tool | Answers | Invocation | |---|---|---| -| `tools/verify_variant_sets.py` | Structural nesting/runtime tuple identity, sentinels and vocabulary; artifact-bound compiler agreement is the distinct, stronger mode | `verify_variant_sets.py --mode {full,structural} [--provenance-root LABEL=DIR]... [--arch A] [--profile P] [--kpack-python-dir D] LABEL ROOT...`. `--mode` is required and has no default: `structural` reports compiled specialization agreement as NOT CHECKED by name and still exits 0 on the rest; `full` fails on a missing, unsupported or mismatched producing-build record **and** on any check that could not run (`GATE FAILED (N check(s) NOT RUN: ...)`). `--profile` supplies the bundle to gate and the matcher vocabulary, which full mode requires over string fields no declaration spells out. `--provenance-root` names the sidecar tree mirroring that label's ROOT, needed for an installed tree, which ships no sidecars; an unknown LABEL or a malformed `LABEL=DIR` exits 2 | +| `tools/verify_variant_sets.py` | Structural nesting/runtime tuple identity, sentinels and vocabulary; artifact-bound compiler agreement is the distinct, stronger mode | `verify_variant_sets.py --mode {full,structural} [--provenance-root LABEL=DIR]... [--arch A] [--profile P] [--kpack-python-dir D] [--generic-targets-json T] LABEL ROOT...`. `--mode` is required and has no default: `structural` reports compiled specialization agreement as NOT CHECKED by name and still exits 0 on the rest; `full` fails on a missing, unsupported or mismatched producing-build record **and** on any check that could not run (`GATE FAILED (N check(s) NOT RUN: ...)`). `--profile` supplies the bundle to gate and the matcher vocabulary, which full mode requires over string fields no declaration spells out. `--provenance-root` names the sidecar tree mirroring that label's ROOT, needed for an installed tree, which ships no sidecars; an unknown LABEL or a malformed `LABEL=DIR` exits 2 | | `tools/variant_reachability.py` | Can any shape in the corpus actually select each variant, or is one dead weight? | `variant_reachability.py --kdp K --shapes S [--provenance-root DIR] [--profile P]`; `--provenance-root` mirrors the tree the KDP is read from, for an installed tree | | `tools/launch_surface.py` | Is every surface the C++ restates from the kernel's Python declared, guarded and tested? | `launch_surface.py PROFILE --check [--allow-unguarded]` | | `tools/coverage_gate.py` | Structural, loading and serving obligations, reported separately; an unmet required obligation cannot pass | `coverage_gate.py --tree T --mode {full,structural} [--provenance-root DIR] [--arch A] [--validator V] [--expect-engine E] [--min-served N]`; `--mode` is required and governs what rung 1 may claim. A missing `--validator` makes rung 2 `loads-not-run`, a failure rather than a skip; an offline result is not serving evidence. `--provenance-root` mirrors `--tree`, for an installed tree, and is forwarded to rung 1 | @@ -512,6 +512,14 @@ Run, in this order, **before any UUID is minted**: `gfx942`), since either looks like an ordinary INFO decline at match time and this tool keeps no exhaustive arch list. +### Generic targets + +Configs accept generic `arch` targets under the +[packaging rules](../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#generic-gpu-targets). +`build_kdp` refuses a tuple shared by two kernels only at the same arch tier +([RFC 0017](../../docs/rfcs/0017_UniversalKernelDescriptor.md#4-resolve-the-kernel-packs-that-name-this-engine-and-apply-the-arch-gate)). The generated census gives every member of +a generic row the generic's descriptors. + ## Source adapters (`codegen/sources/`) One protocol (`SourceAdapter.infer(*sources) -> SourceAdapterResult`), two v1 @@ -527,9 +535,8 @@ producing compiler. `hsaco` authoring also uses the packaged path: `kernel_sourc descriptor that names it and `symbol` its kernel. `file` must stay inside the source root, with no root-relative fallback. `hkp_pack` packs that object as-is, without compiling, so like `hip` the specialization declares `metadata_fields: []`. The packer does not check the -object's format or target processor: every hsaco kernel must carry a non-empty per-kernel `arch` listing the -arch(es) its object runs on (a generic-target object lists each one), and the loader -rejects one without. +object's format or target processor; `arch` follows +[hsaco kernels and `arch`](../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#hsaco-kernels-and-arch). `hsaco_file` is rejected explicitly, naming `supportsSourceKind()` as the missing prerequisite on `IKernelDispatchHandler`. diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py index aa0e34fa2bf0..5b266e59af72 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/config_loader.py @@ -19,6 +19,7 @@ import yaml +from . import generic_targets as gtmod from .models import ( ARCH_BASE_ID_PATTERN, AUTHORED_TEST_SETS, @@ -1364,19 +1365,106 @@ def _check_kernel_priority(config: IngestorConfig) -> None: ) +def _generic_table() -> "gtmod.GenericTargets": + """The generic target table; a read failure is a ConfigError.""" + try: + return gtmod.default_table() + except ValueError as exc: + raise ConfigError(str(exc)) from exc + + +def _check_arch_list_generics(archs: list, where: str, table) -> None: + """Per-list rules: no unknown generics, no generic beside its member or a generic sharing one.""" + for entry in archs: + if gtmod.is_generic_shaped(entry) and not table.has(entry): + raise ConfigError( + f"{where}: arch entry '{entry}' is a generic target name absent " + f"from the generic target table {table.path} " + f"(known: {', '.join(table.names())})" + ) + advice = ( + "list one, or author two packs (an explicit override plus a generic " + "fallback are separate packs)" + ) + for i, x in enumerate(archs): + for y in archs[i + 1 :]: + if table.has(x) and table.has(y): + shared = [m for m in table.members(x) if m in table.members(y)] + if shared: + raise ConfigError( + f"{where}: 'arch' lists '{x}' and '{y}': the two generics " + f"share member {shared[0]}; {advice}" + ) + continue + generic, member = (x, y) if table.has(x) else (y, x) + if table.has(generic) and member in table.members(generic): + raise ConfigError( + f"{where}: 'arch' lists '{x}' and '{y}': {generic} contains " + f"{member}; {advice}" + ) + + +def _check_generic_arch(config: IngestorConfig) -> None: + """Pre-mint check #4a: the packer's generic-target rules (see descriptor-packaging/README.md).""" + table = _generic_table() + for pack in config.packs: + where = f"pack '{pack.name}'" + _check_arch_list_generics(pack.arch, where, table) + pack_generics = [a for a in pack.arch if gtmod.is_generic_shaped(a)] + for kernel in pack.kernels: + kernel_where = f"{where} kernel '{kernel.name}'" + _check_arch_list_generics(kernel.arch, kernel_where, table) + if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_ROCKE: + rocke_generics = [ + a + for a in (pack_generics or kernel.arch) + if gtmod.is_generic_shaped(a) + ] + if rocke_generics: + raise ConfigError( + f"{kernel_where}: rocKE does not support generic targets " + f"yet (arch '{rocke_generics[0]}')" + ) + stamped = ( + list(kernel.arch or pack.arch) + if kernel.kernel_source.kind == KERNEL_SOURCE_KIND_HSACO + else list(kernel.arch) + ) + for generic in stamped: + if gtmod.is_generic_shaped(generic) and generic not in pack.arch: + raise ConfigError( + f"{kernel_where}: declares generic target '{generic}' but " + f"the pack does not list '{generic}'; it would ship in no " + f"shard" + ) + if not pack_generics or not stamped: + continue + lists_all = all(g in stamped for g in pack_generics) + only_listed = all(a in pack.arch for a in stamped) + if not lists_all or not only_listed: + raise ConfigError( + f"{where}: pack lists generic target(s) {pack_generics} (arch " + f"{pack.arch}) but kernel '{kernel.name}' declares arch " + f"{stamped}; a kernel under a generic pack must list every " + f"generic of the KDP and only entries the KDP lists" + ) + + def _check_kernel_arch_subset_of_pack(config: IngestorConfig) -> None: """Pre-mint check #4: a kernel's arch must be a subset of its pack's. - Mirrors ``DescriptorLoader.hpp``'s ``archCovers(pack.arch, kernel.arch)``. - An empty pack.arch covers everything; an empty kernel.arch inherits it. + Mirrors ``ingestor/DeviceProperties.hpp``'s ``archCovers`` over expanded device sets. """ + table = _generic_table() for pack in config.packs: if not pack.arch: continue for kernel in pack.kernels: if not kernel.arch: continue - reaching = [a for a in kernel.arch if a not in pack.arch] + reaching = [ + a for a in kernel.arch if not gtmod.covers(pack.arch, [a], table) + ] if reaching: raise ConfigError( f"pack '{pack.name}' kernel '{kernel.name}' declares arch " @@ -1396,6 +1484,7 @@ def _check_arch_shape(config: IngestorConfig) -> list[str]: also raised via ``warnings.warn``. """ messages: list[str] = [] + table = _generic_table() def check_list(archs: list, where: str) -> None: for arch in archs: @@ -1409,7 +1498,7 @@ def check_list(archs: list, where: str) -> None: f"parses fine and then declines on every device, logging " f"exactly what a healthy cross-arch install logs." ) - if arch not in KNOWN_ARCH_BASE_IDS: + if arch not in KNOWN_ARCH_BASE_IDS and not table.has(arch): message = ( f"{where} arch entry '{arch}' is well-formed but not a " f"recognized gfx target id (e.g. a typo like 'gfx94' for " @@ -1954,6 +2043,7 @@ def _validate_config(config: IngestorConfig) -> list[str]: # KMD_FIELD_TYPES; the default_value check indexes _METADATA_TYPE_CHECKS by it. _check_kmd_default_values(config) _check_kernel_priority(config) + _check_generic_arch(config) _check_kernel_arch_subset_of_pack(config) # #4 warnings_out = _check_arch_shape(config) # #5 diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py index 8f2f5f5ae63a..8ded1ef6af24 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generator.py @@ -14,6 +14,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined +from . import generic_targets as gtmod from .models import ( KERNEL_SOURCE_KIND_HSACO, KERNEL_SOURCE_KIND_KPACK, @@ -336,8 +337,7 @@ def _dedup_key(metadata: dict, config: IngestorConfig) -> str: """Identity of a descriptor as the matcher sees it: the completed tuple, not the emitted document. - Architecture is not part of this key; `build_kdp` decides overlap against - the arch coverage recorded beside each key. + Architecture is not part of this key; `build_kdp` compares arch separately. """ return json.dumps(_completed_metadata(metadata, config), sort_keys=True) @@ -356,11 +356,9 @@ def _candidate_identity(kernel: KernelSpec) -> str: ) -def _arch_overlaps(left: list, right: list) -> bool: - """Whether two arch coverages can select on the same device. An empty list - is the loader's wildcard and overlaps everything. Mirrors - ``hkp_pack.agreement.overlap``.""" - return not left or not right or bool(set(left) & set(right)) +def _arch_competes(left: list, right: list) -> bool: + """Whether two arch lists tie on some device at the same tier.""" + return gtmod.compete(left, right, gtmod.default_table()) def _pack_index(config: IngestorConfig, pack: PackSpec) -> int: @@ -451,9 +449,8 @@ def build_kdp( if config.is_multi_pack: matchers.insert(0, ids[("operation_umd", pack_index)]) # Generation expressions targeting one engine may overlap, so a shared - # tuple has four outcomes: disjoint arch coverage is not a duplicate; - # overlapping coverage with the same candidate and equal arch de-duplicates - # here; a different candidate, or unequal coverage, is refused. + # tuple is a duplicate only when arch coverage competes: the same candidate + # with equal arch de-duplicates, anything else is refused. kernel_descriptors = [] if seen_metadata is None: seen_metadata = {} @@ -475,7 +472,7 @@ def build_kdp( identity = _candidate_identity(kernel) already = None for prior in seen_metadata.setdefault(key, []): - if not _arch_overlaps(arch, prior["arch"]): + if not _arch_competes(arch, prior["arch"]): continue if prior["identity"] == identity: if sorted(prior["arch"]) == sorted(arch): @@ -485,19 +482,19 @@ def build_kdp( f"kernel {kernel.name!r} (pack {pack.name!r}) and kernel " f"{prior['name']!r} (pack {prior['pack']!r}) are the SAME " f"candidate and complete to the SAME catalog tuple " - f"{json.loads(key)}, but their architectures overlap without " - f"being equal ({arch or ['']} vs " + f"{json.loads(key)}, but they compete at the same arch tier " + f"without having equal arch lists ({arch or ['']} vs " f"{prior['arch'] or ['']}). On the shared architectures " f"the matcher would see one tuple twice, which drops the whole " f"engine at load; coalescing them would instead advertise one " f"of the two on devices its arch list never claimed. Give the " - f"two entries the SAME arch list so they de-duplicate, or make " - f"them disjoint." + f"two entries the SAME arch list so they de-duplicate, or put " + f"them at different arch tiers." ) raise ValueError( f"kernel {kernel.name!r} (pack {pack.name!r}) and kernel " f"{prior['name']!r} (pack {prior['pack']!r}) complete to the SAME " - f"catalog tuple {json.loads(key)} on overlapping architectures " + f"catalog tuple {json.loads(key)} at the same arch tier " f"({arch or ['']} vs {prior['arch'] or ['']}), but they " f"are not the same candidate: their kernel_source/priority differ, " f"so they name different binaries. The matcher compares the tuple " @@ -505,7 +502,8 @@ def build_kdp( f"whole engine at load, and dropping one of them here would " f"discard a binary that was deliberately built. Distinguish them " f"in metadata (a knob the spec pins belongs in the tuple), narrow " - f"one of the arch lists, or do not ship both." + f"one of the arch lists, move one to a different arch tier, or do " + f"not ship both." ) if already is not None: # Carry the prior kernel's own pack: a drop that empties this pack @@ -616,8 +614,8 @@ def emitted_inventory(config: IngestorConfig, kdp_documents: list) -> dict: lowered to ``kpack`` first. A descriptor with no ``arch`` is filed under its pack's; a pack with none under `ARCH_WILDCARD`. - Each concrete arch row is unioned with the wildcard row, one way only, - because a wildcard entry ships on that device too. + Each generic's descriptors also fill the rows of its members; the wildcard + row fills every row. """ arches: dict[str, dict] = {} @@ -635,6 +633,21 @@ def bucket(arch: str) -> dict: for arch in list(descriptor.get("arch") or []) or pack_arch: bucket(arch)["descriptors"].append(descriptor["name"]) + table = gtmod.default_table() + generic_rows = { + name: (list(arches[name]["descriptors"]), set(arches[name]["pack_names"])) + for name in table.names() + if name in arches + } + for name in generic_rows: + for member in table.members(name): + bucket(member) + for arch, entry in arches.items(): + for name, (descriptors, pack_names) in generic_rows.items(): + if arch in table.members(name): + entry["descriptors"].extend(descriptors) + entry["pack_names"].update(pack_names) + wildcard = arches.get(ARCH_WILDCARD) if wildcard is not None: for arch, entry in arches.items(): diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py new file mode 100644 index 000000000000..d296b2e021de --- /dev/null +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/generic_targets.py @@ -0,0 +1,147 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +"""Generic GPU target table and arch-list tier algebra; see descriptor-packaging/README.md. + +Mirrors ``hkp_pack.generic_targets`` and the C++ ``archTier`` (not importable from here). +""" + +import json +from functools import cache +from pathlib import Path + +TIER_EXPLICIT = 0 +TIER_GENERIC = 1 +TIER_UNRESTRICTED = 2 + +DEFAULT_TABLE_PATH = ( + Path(__file__).resolve().parents[3] + / "plugin_sdk" + / "data" + / "gpu_generic_targets.json" +) + +_GENERIC_SUFFIX = "-generic" + + +class GenericTargets: + def __init__(self, path: Path, generics: dict): + self._path = Path(path) + self._generics = generics + + @classmethod + def load(cls, path) -> "GenericTargets": + """Read the table at @path; ``ValueError`` on any defect.""" + path = Path(path) + try: + doc = json.loads(path.read_text(encoding="utf-8")) + except OSError as exc: + raise ValueError( + f"{path}: cannot read generic target table: {exc}" + ) from exc + except ValueError as exc: + raise ValueError( + f"{path}: generic target table is not valid JSON: {exc}" + ) from exc + raw = doc.get("generics") if isinstance(doc, dict) else None + if not isinstance(raw, dict): + raise ValueError(f"{path}: 'generics' must be an object") + generics = {} + for name, members in raw.items(): + if ( + not isinstance(members, list) + or not members + or not all(isinstance(m, str) and m for m in members) + ): + raise ValueError( + f"{path}: generics.{name} must be a non-empty array of names" + ) + generics[name] = tuple(members) + return cls(path, generics) + + @property + def path(self) -> Path: + return self._path + + def has(self, name: str) -> bool: + return name in self._generics + + def members(self, name: str) -> tuple: + return self._generics[name] + + def names(self) -> tuple: + return tuple(self._generics) + + +@cache +def default_table() -> GenericTargets: + return GenericTargets.load(DEFAULT_TABLE_PATH) + + +def is_generic_shaped(name: str) -> bool: + return name.endswith(_GENERIC_SUFFIX) + + +def _strip_features(arch: str) -> str: + return arch.split(":", 1)[0] + + +def entry_tier(entry: str, device: str, table: GenericTargets): + """Tier of one entry for @device, or None when it does not admit it.""" + if is_generic_shaped(entry): + if table.has(entry) and _strip_features(device) in table.members(entry): + return TIER_GENERIC + return None + if device == entry or device.startswith(entry + ":"): + return TIER_EXPLICIT + return None + + +def list_tier(entries, device: str, table: GenericTargets): + """Best tier of @entries for @device; None = no match.""" + if not entries: + return TIER_UNRESTRICTED + tiers = [ + t for t in (entry_tier(e, device, table) for e in entries) if t is not None + ] + return min(tiers) if tiers else None + + +def expand(entries, table: GenericTargets): + """Device ids @entries admits; None when unrestricted.""" + if not entries: + return None + devices: set = set() + for entry in entries: + if is_generic_shaped(entry): + if table.has(entry): + devices.update(table.members(entry)) + else: + devices.add(entry) + return frozenset(devices) + + +def covers(outer, inner, table: GenericTargets) -> bool: + """Is every device @inner admits also admitted by @outer?""" + expanded_outer, expanded_inner = expand(outer, table), expand(inner, table) + if expanded_outer is None or expanded_inner is None: + return True + return expanded_inner <= expanded_outer + + +def compete(a, b, table: GenericTargets) -> bool: + """Do the lists match some device at the same tier?""" + if not a and not b: + return True + candidates: set = set() + for entry in (*a, *b): + if is_generic_shaped(entry): + if table.has(entry): + candidates.update(table.members(entry)) + else: + candidates.add(entry) + for device in candidates: + tier = list_tier(a, device, table) + if tier is not None and tier == list_tier(b, device, table): + return True + return False diff --git a/projects/hipdnn/tools/IngestorGenerator/codegen/models.py b/projects/hipdnn/tools/IngestorGenerator/codegen/models.py index 21c983ef0ca8..ae62cf8d6b1a 100644 --- a/projects/hipdnn/tools/IngestorGenerator/codegen/models.py +++ b/projects/hipdnn/tools/IngestorGenerator/codegen/models.py @@ -7,6 +7,8 @@ from dataclasses import dataclass, field from typing import Optional +from . import generic_targets as gtmod + #: KMD field types the loader accepts (``DescriptorLoader.hpp``'s ``MetadataField``). KMD_FIELD_TYPES: tuple[str, ...] = ("bool", "int", "float", "string", "int_list") @@ -388,9 +390,13 @@ def kdp_stem(self, pack: PackSpec) -> str: def device_fixture_arch(self) -> str: """Architecture named by the emitted matcher-test device fixture: the first pack's first arch, or `DEFAULT_FIXTURE_ARCH` when the config - restricts none.""" + restricts none; a generic stands for its first member.""" first_pack_arch = self.packs[0].arch if self.packs else [] - return first_pack_arch[0] if first_pack_arch else DEFAULT_FIXTURE_ARCH + if not first_pack_arch: + return DEFAULT_FIXTURE_ARCH + first = first_pack_arch[0] + table = gtmod.default_table() + return table.members(first)[0] if table.has(first) else first @property def device_fixture_wave_size(self) -> int: diff --git a/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 b/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 index 32eb166ece54..7d8a655fcd10 100644 --- a/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 +++ b/projects/hipdnn/tools/IngestorGenerator/templates/test_packs.cpp.j2 @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -92,13 +93,6 @@ constexpr ingestor::KernelSourceKind EXPECTED_SOURCE_KIND 'kpack': 'KPACK', 'hsaco_file': 'HSACO_FILE'}[emitted.source_kind] }}; -/// A pack or kernel naming no architecture inherits/wildcards; one that names -/// architectures must name the requested one, or this is the wrong shard. -bool coversArch(const std::vector& declared, const std::string& arch) -{ - return declared.empty() || std::find(declared.begin(), declared.end(), arch) != declared.end(); -} - class Test{{ config.engine.pascal_name }}Packs : public ::testing::Test { protected: @@ -169,12 +163,12 @@ TEST_F(Test{{ config.engine.pascal_name }}Packs, EveryLoadedDescriptorCoversTheR { for(const auto& pack : _set->packs) { - EXPECT_TRUE(coversArch(pack.arch, _arch)) + EXPECT_TRUE(ingestor::archSupports(pack.arch, _arch)) << "pack '" << pack.name << "' does not cover '" << _arch << "', so this is not that architecture's shard"; for(const auto& kernel : pack.kernels) { - EXPECT_TRUE(coversArch(kernel.arch, _arch)) + EXPECT_TRUE(ingestor::archSupports(kernel.arch, _arch)) << "kernel '" << kernel.name << "' does not cover '" << _arch << "'"; } } diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py index e7665f426af4..f75395e85662 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_config_loader.py @@ -5,6 +5,9 @@ configs, the five pre-mint loader-mirroring checks, deprecated- and duplicate-key rejection, and the kernel_source_kind rejections.""" +import sys +from pathlib import Path + import pytest import yaml @@ -189,6 +192,77 @@ def test_empty_pack_arch_covers_everything(self): _check_kernel_arch_subset_of_pack(config) # does not raise +class TestGenericArchRules: + """Generic-target rules; ``gfx11-generic`` holds gfx1100..gfx1153, ``gfx12-generic`` gfx1200/gfx1201.""" + + @staticmethod + def _config(pack_arch, kernel_arch, kind=None): + extra = {"kernel_source": KernelSource(kind=kind)} if kind else {} + kernel = make_kernel(arch=kernel_arch, **extra) + return make_minimal_config(packs=[make_pack(arch=pack_arch, kernels=[kernel])]) + + @pytest.mark.parametrize( + ("pack_arch", "kernel_arch"), + [ + (["gfx11-generic"], []), + (["gfx11-generic"], ["gfx11-generic"]), + (["gfx942", "gfx11-generic"], ["gfx942", "gfx11-generic"]), + ], + ) + def test_accepted_shapes(self, pack_arch, kernel_arch): + from codegen.config_loader import _check_generic_arch + + _check_generic_arch(self._config(pack_arch, kernel_arch)) + + @pytest.mark.parametrize( + ("pack_arch", "kernel_arch"), + [ + (["gfx9-4-generic"], []), + ([], ["gfx9-4-generic"]), + (["gfx11-generic", "gfx1151"], []), + (["gfx1151", "gfx11-generic"], []), + (["gfx942", "gfx11-generic"], ["gfx1151", "gfx11-generic"]), + ([], ["gfx11-generic"]), + (["gfx942"], ["gfx11-generic"]), + (["gfx11-generic"], ["gfx1151"]), + (["gfx11-generic", "gfx12-generic"], ["gfx11-generic"]), + ], + ) + def test_rejected_shapes(self, pack_arch, kernel_arch): + from codegen.config_loader import _check_generic_arch + + with pytest.raises(ConfigError): + _check_generic_arch(self._config(pack_arch, kernel_arch)) + + @pytest.mark.parametrize( + ("pack_arch", "kernel_arch"), + [ + (["gfx11-generic"], []), + (["gfx942", "gfx11-generic"], []), + ([], ["gfx11-generic"]), + ], + ) + def test_rocke_under_a_generic_is_rejected(self, pack_arch, kernel_arch): + from codegen.config_loader import _check_generic_arch + + with pytest.raises(ConfigError): + _check_generic_arch(self._config(pack_arch, kernel_arch, kind="rocke")) + + def test_known_generic_produces_no_unrecognised_arch_warning(self): + from codegen.config_loader import _check_arch_shape + + assert _check_arch_shape(self._config(["gfx11-generic"], [])) == [] + + def test_an_expanded_subset_is_accepted_by_the_coverage_check(self): + from codegen.config_loader import _check_kernel_arch_subset_of_pack + + _check_kernel_arch_subset_of_pack(self._config(["gfx11-generic"], ["gfx1151"])) + with pytest.raises(ConfigError): + _check_kernel_arch_subset_of_pack( + self._config(["gfx11-generic"], ["gfx1250"]) + ) + + class TestArchShapeCheck: """Pre-mint check #5: arch entries are plausible gfx-prefixed base ids (error), and unrecognized-but-well-formed ids warn rather than error.""" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py index d3bd20a0d107..f47dfb5cced0 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generator.py @@ -725,45 +725,42 @@ def test_equal_tuples_on_disjoint_arches_both_survive(self): "twin.right", ] - def test_equal_tuples_on_overlapping_arches_are_refused_naming_both(self): - """Same tuple, same device, DIFFERENT binary: dropping the second discards a - binary somebody built, keeping both drops the engine at load, so the generator - names both.""" + @pytest.mark.parametrize( + ("pack_arch", "left_arch", "right_arch", "other_binary"), + [ + (["gfx942"], [], [], True), + (["gfx942", "gfx950"], ["gfx942"], ["gfx942", "gfx950"], False), + (["gfx11-generic"], [], [], True), + ], + ) + def test_equal_tuples_tied_on_a_device_are_refused_naming_both( + self, pack_arch, left_arch, right_arch, other_binary + ): config, pack, left, right = self._twin_config() - # Same matcher-visible metadata, different compiled source. - right.kernel_source.entry_point = "ScaleAddOther" + pack.arch = pack_arch + left.arch = left_arch + right.arch = right_arch + if other_binary: + right.kernel_source.entry_point = "ScaleAddOther" with pytest.raises(ValueError) as excinfo: build_kdp(config, pack, mint_ids(config)) message = str(excinfo.value) assert "twin.left" in message and "twin.right" in message - def test_one_candidate_on_unequal_overlapping_arches_names_the_coverage(self): - """Same tuple, same device, SAME binary: there is no kernel_source or priority - difference to find, so the diagnostic must name the coverage.""" - config, pack, left, right = self._twin_config() - pack.arch = ["gfx942", "gfx950"] - left.arch = ["gfx942"] - right.arch = ["gfx942", "gfx950"] - - with pytest.raises(ValueError) as excinfo: - build_kdp(config, pack, mint_ids(config)) - message = str(excinfo.value) - assert "SAME candidate" in message - assert "kernel_source/priority differ" not in message - - def test_a_wildcard_arch_overlapping_a_concrete_one_is_refused(self): - """An absent ``arch`` is the loader's wildcard: treated as no coverage it sits - beside a concrete candidate holding the same tuple and collides on that - device.""" + def test_a_wildcard_arch_and_a_concrete_one_with_one_tuple_both_survive(self): + """A concrete arch outranks an empty one on its own device, so they never tie.""" config, pack, left, right = self._twin_config() pack.arch = [] left.arch = [] # every device right.arch = ["gfx942"] right.kernel_source.entry_point = "ScaleAddOther" - with pytest.raises(ValueError, match="overlapping architectures"): - build_kdp(config, pack, mint_ids(config)) + kdp = build_kdp(config, pack, mint_ids(config)) + assert [k["name"] for k in kdp["kernelDescriptors"]] == [ + "twin.left", + "twin.right", + ] def test_an_omitted_field_and_the_kmd_default_are_one_key(self): """One tuple omits an optional field; the other states it at exactly the KMD @@ -1034,6 +1031,42 @@ def test_two_distinct_arches_each_count_the_descriptor_once(self, tmp_path): assert inventory["arches"]["gfx950"]["descriptor_count"] == 1 assert inventory["total_descriptor_count"] == 1 + def test_inventory_gives_every_member_the_generic_rows(self): + """Each member's row holds the generic's descriptors, its own, and the wildcard's.""" + generic = make_pack( + name="generic", + arch=["gfx11-generic"], + kernels=[make_kernel(name="generic.k", metadata={"block_size": 64})], + ) + explicit = make_pack( + name="explicit", + arch=["gfx1151"], + kernels=[make_kernel(name="explicit.k", metadata={"block_size": 128})], + ) + other = make_pack( + name="other", + arch=["gfx942"], + kernels=[make_kernel(name="other.k", metadata={"block_size": 256})], + ) + everywhere = make_pack( + name="everywhere", + arch=[], + kernels=[make_kernel(name="any.k", metadata={"block_size": 512})], + ) + config = make_minimal_config(packs=[generic, explicit, other, everywhere]) + documents = build_kdp_documents(config, mint_ids(config)) + arches = emitted_inventory(config, documents)["arches"] + + names = {a: set(row["descriptor_names"]) for a, row in arches.items()} + assert names["gfx1100"] == {"generic.k", "any.k"} + assert names["gfx1153"] == {"generic.k", "any.k"} + assert names["gfx1151"] == {"generic.k", "explicit.k", "any.k"} + assert names["gfx942"] == {"other.k", "any.k"} + assert names["gfx11-generic"] == {"generic.k", "any.k"} + assert "gfx1154" not in names + stems = {config.kdp_stem(p) for p in (generic, everywhere)} + assert set(arches["gfx1100"]["pack_names"]) == stems + def test_the_packaged_dialect_reports_the_kind_that_actually_ships(self): """hkp_pack lowers ``rocke``/``hip`` to ``kpack`` before the loader reads it, so a census expecting the AUTHORED kind expects one that never arrives.""" @@ -1717,6 +1750,26 @@ def test_an_expansion_path_is_not_double_rejected( class TestPackagedHsacoKdp: + def test_hsaco_kernel_inheriting_a_generic_pack_arch_conforms_to_the_pack_rule( + self, + ): + from codegen.config_loader import _check_generic_arch + + kernel = make_kernel( + name="test.hsaco", + kernel_source=KernelSource( + kind="hsaco", file="HsacoFixture.co", symbol="HsacoFixtureAdd" + ), + ) + config = make_minimal_config( + dialect="packaged", + kernel_source_kind="hsaco", + packs=[make_pack(kernels=[kernel], arch=["gfx942", "gfx11-generic"])], + ) + _check_generic_arch(config) + kdp = build_kdp(config, config.packs[0], mint_ids(config)) + assert kdp["kernelDescriptors"][0]["arch"] == ["gfx942", "gfx11-generic"] + def test_every_hsaco_ukd_carries_exactly_kind_file_symbol(self): """hkp_pack validates a closed field set per kind, so a UKD carrying any other kind's key is refused at pack time.""" diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py new file mode 100644 index 000000000000..d5c372fce09f --- /dev/null +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_generic_targets.py @@ -0,0 +1,94 @@ +# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT + +"""Tier algebra over a small in-memory table.""" + +from pathlib import Path + +import pytest + +from codegen import generic_targets as gt + +_TABLE = gt.GenericTargets( + Path("table.json"), + { + "gfx11-generic": ("gfx1100", "gfx1151"), + "gfx12-generic": ("gfx1200", "gfx1201"), + }, +) +_G11 = "gfx11-generic" +_G12 = "gfx12-generic" +_UNKNOWN = "gfx9-4-generic" + +_UNRESTRICTED = gt.TIER_UNRESTRICTED +_EXPLICIT = gt.TIER_EXPLICIT +_GENERIC = gt.TIER_GENERIC + + +@pytest.mark.parametrize( + ("arch", "device", "expect"), + [ + ([], "gfx1151", _UNRESTRICTED), + (["gfx1151"], "gfx1151", _EXPLICIT), + ([_G11], "gfx1100", _GENERIC), + ([_G11], "gfx1151:sramecc+", _GENERIC), + (["gfx1151", _G11], "gfx1151", _EXPLICIT), + (["gfx1151", _G11], "gfx1100", _GENERIC), + ([_G11], "gfx1154", None), + ([_G12], "gfx1100", None), + (["gfx942"], "gfx950", None), + (["gfx94"], "gfx942", None), + (["gfx1250"], "gfx1250-strict", None), + (["gfx1250-strict"], "gfx1250-strict", _EXPLICIT), + ([_UNKNOWN], "gfx942", None), + ], +) +def test_list_tier(arch, device, expect): + assert gt.list_tier(arch, device, _TABLE) == expect + + +@pytest.mark.parametrize( + ("outer", "inner", "expect"), + [ + ([], ["gfx942"], True), + (["gfx942"], [], True), + (["gfx942", "gfx950"], ["gfx942"], True), + (["gfx942"], ["gfx942", "gfx950"], False), + ([_G11], ["gfx1151"], True), + ([_G11], ["gfx1154"], False), + (["gfx1151"], [_G11], False), + (["gfx1100", "gfx1151"], [_G11], True), + ([_G11], [_G11], True), + ([_G11], [_G12], False), + (["gfx942", _G11], ["gfx942", "gfx1100"], True), + (["gfx942", _G11], ["gfx950", "gfx1100"], False), + ], +) +def test_covers(outer, inner, expect): + assert gt.covers(outer, inner, _TABLE) is expect + + +@pytest.mark.parametrize( + ("a", "b", "expect"), + [ + ([], [], True), + ([], ["gfx942"], False), + (["gfx942"], ["gfx942"], True), + (["gfx942"], ["gfx950"], False), + ([_G11], ["gfx1151"], False), + ([_G11], [_G11], True), + ([_G11], [_G12], False), + (["gfx1151", _G11], [_G11], True), + ([_G11], [_G11, "gfx942"], True), + ], +) +def test_compete(a, b, expect): + assert gt.compete(a, b, _TABLE) is expect + assert gt.compete(b, a, _TABLE) is expect + + +def test_the_shipped_table_loads_and_lists_gfx11_generic_members(): + table = gt.default_table() + assert table.has(_G11) + assert "gfx1151" in table.members(_G11) + assert gt.list_tier([_G11], "gfx1151", table) == _GENERIC diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py index 34b861677a9e..8f4bebe2d772 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_models.py @@ -119,6 +119,10 @@ def test_device_fixture_arch_is_the_first_packs_first_arch(self): config = make_minimal_config(packs=[make_pack(arch=["gfx1201", "gfx1200"])]) assert config.device_fixture_arch == "gfx1201" + def test_device_fixture_arch_of_a_generic_pack_is_its_first_member(self): + config = make_minimal_config(packs=[make_pack(arch=["gfx11-generic"])]) + assert config.device_fixture_arch == "gfx1100" + def test_device_fixture_arch_falls_back_when_no_pack_names_one(self): config = make_minimal_config(packs=[make_pack()]) assert not config.packs[0].arch diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py index c064b478a36d..1a092e3585e7 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_native_stub.py @@ -4,8 +4,9 @@ """A real compiler's verdict on the emitted C++. ``packs/Native.cpp`` is what an agent or a human fills in to make an engine serve real graphs (see RUNBOOK.md).""" -import subprocess +import json import shutil +import subprocess import tempfile from pathlib import Path @@ -78,6 +79,29 @@ def compile_env(): out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(text) + # GpuGenericTargetsTable.hpp is rendered from the JSON table, as plugin_sdk/CMakeLists.txt does. + table_in = (plugin_sdk / ".." / "cmake" / "GpuGenericTargetsTable.hpp.in").resolve() + table_json = (plugin_sdk / ".." / "data" / "gpu_generic_targets.json").resolve() + if not (table_in.is_file() and table_json.is_file()): + pytest.skip(f"missing generic target table inputs {table_in} / {table_json}") + generics = json.loads(table_json.read_text())["generics"] + arrays, rows = "", "" + for generic in sorted(generics): + array = generic.upper().replace("-", "_") + "_MEMBERS" + literals = "".join(f' "{m}",\n' for m in generics[generic]) + arrays += f"inline constexpr std::string_view {array}[] = {{\n{literals}}};\n\n" + rows += f' {{"{generic}", {array}, {len(generics[generic])}}},\n' + table_text = table_in.read_text() + for key, value in ( + ("HIPDNN_GENERIC_TARGET_MEMBER_ARRAYS", arrays), + ("HIPDNN_GENERIC_TARGET_ROWS", rows), + ("HIPDNN_GENERIC_TARGET_ROW_COUNT", str(len(generics))), + ): + table_text = table_text.replace(f"@{key}@", value) + table_out = gen_dir / "hipdnn_plugin_sdk" / "GpuGenericTargetsTable.hpp" + table_out.parent.mkdir(parents=True, exist_ok=True) + table_out.write_text(table_text) + for name, root, version_vals in ( ( "hipdnn_data_sdk", diff --git a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py index ffb4309b2bac..d765873a8889 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tests/test_verify_variant_sets.py @@ -416,8 +416,9 @@ def test_tuple_check_substitutes_kmd_defaults_like_the_loader(self, gate): class TestTheDeskCheckIdentityIsEngineWideAndArchAware: """The loader assembles ONE catalog per engine per device, so the identity is engine-wide and carries the effective architecture: an identical tuple in a gfx942 - and a gfx950 pack is legal, while an overlap -- including a wildcard over a concrete - arch -- drops the engine.""" + and a gfx950 pack is legal, while two packs that compete at the same arch tier drop the + engine. A wildcard over a concrete arch, or a generic over a member, sit at + different tiers and do not compete.""" def _two_packs(self, gate, tag, left_arch, right_arch): root = gate.tmp / tag @@ -440,7 +441,6 @@ def test_equal_tuples_on_disjoint_arches_are_both_accepted(self, gate): self._two_packs(gate, "disjoint", ["gfx942"], ["gfx950"]) result = gate.run("d", "disjoint", profiled=False) assert result.returncode == 0, result.stdout + result.stderr - assert "loader-tuple" not in result.stdout def test_equal_tuples_overlapping_on_one_arch_collide(self, gate): self._two_packs(gate, "overlap", ["gfx942", "gfx950"], ["gfx950"]) @@ -449,11 +449,27 @@ def test_equal_tuples_overlapping_on_one_arch_collide(self, gate): assert "loader-tuple collisions" in result.stdout assert "gfx950" in result.stdout - def test_a_wildcard_arch_overlaps_a_concrete_one(self, gate): - """An absent arch list is "every device", so it meets the concrete pack on that - pack's own device.""" - self._two_packs(gate, "wildcard", None, ["gfx950"]) - result = gate.run("w", "wildcard", profiled=False) + @pytest.mark.parametrize( + ("left_arch", "right_arch"), + [ + (None, ["gfx950"]), + (["gfx11-generic"], ["gfx1151"]), + ], + ) + def test_a_better_tier_does_not_collide_with_a_worse_one( + self, gate, left_arch, right_arch + ): + self._two_packs(gate, "outranked", left_arch, right_arch) + result = gate.run("o", "outranked", profiled=False) + assert result.returncode == 0, result.stdout + result.stderr + + @pytest.mark.parametrize( + ("left_arch", "right_arch"), + [(["gfx11-generic"], ["gfx11-generic"]), (None, None)], + ) + def test_two_packs_tied_at_one_tier_collide(self, gate, left_arch, right_arch): + self._two_packs(gate, "tied", left_arch, right_arch) + result = gate.run("t", "tied", profiled=False) assert result.returncode == 1, result.stdout assert "loader-tuple collisions" in result.stdout @@ -1147,18 +1163,18 @@ def real_archive(): kpack, compression = load_kpack(python_dir) - def write(root): + def write(root, arch=_ARCH): path = root / "kpack" / "test.kpack" path.parent.mkdir(parents=True, exist_ok=True) archive = kpack.PackedKernelArchive( group_name="test", - gfx_arch_family=_ARCH, - gfx_arches=[_ARCH], + gfx_arch_family=arch, + gfx_arches=[arch], compressor=compression.ZstdCompressor(compression_level=3), ) archive.add_kernel( archive.prepare_kernel( - relative_path="v0", gfx_arch=_ARCH, hsaco_data=_PAYLOAD, metadata={} + relative_path="v0", gfx_arch=arch, hsaco_data=_PAYLOAD, metadata={} ) ) archive.finalize_archive() @@ -1340,3 +1356,36 @@ def test_unclaimed_hip_must_still_be_packed(self, tmp_path, real_archive): assert "packed dialect" in result.stdout structural = self.run(root, python_dir, tmp_path, mode="structural") assert structural.returncode == 0, structural.stdout + structural.stderr + + +@pytest.mark.needs_rocm_kpack +class TestAGenericDocumentIsReadWithItsGenericArchiveKey: + """A generic copy's archive is keyed by the generic's name.""" + + @staticmethod + def _entry(arch): + ukd = _packed_ukd() + ukd["arch"] = arch + return type( + "Entry", (), {"ukd": ukd, "arch": arch, "origin_dir": None, "inline": False} + ) + + def test_a_generic_document_is_read_with_its_generic_archive_key( + self, tmp_path, real_archive + ): + write_archive, python_dir = real_archive + write_archive(tmp_path, "gfx11-generic") + + table = gate_module.gtmod.GenericTargets.load( + gate_module.gtmod.DEFAULT_TABLE_PATH + ) + payloads = gate_module.Payloads(python_dir, table) + + generic = self._entry(["gfx11-generic"]) + generic.origin_dir = str(tmp_path) + assert payloads.read(generic, "gfx1151") == _PAYLOAD + + member = self._entry(["gfx1151"]) + member.origin_dir = str(tmp_path) + with pytest.raises(gate_module.GateError): + payloads.read(member, "gfx1151") diff --git a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py index 6925d64a45d9..545b84bda46e 100644 --- a/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py +++ b/projects/hipdnn/tools/IngestorGenerator/tools/verify_variant_sets.py @@ -5,8 +5,8 @@ 1. BINARY NESTING -- the larger set can still choose every binary the smaller one could. 2. LOADER-TUPLE UNIQUENESS -- with absent keys filled from the KMD - `default_value`, each tuple is unique per device. A duplicate rejects the - whole engine at load. + `default_value`, each tuple is unique per device and tier (explicit, + generic, unrestricted). A duplicate rejects the whole engine at load. 3. NO SENTINEL -- `-1` never reaches a shipped descriptor. 4. METADATA MATCHES ITS BINARY -- the matcher selects on metadata, the spec decides what was built. @@ -51,6 +51,7 @@ def _agreement_python_root() -> Path: sys.path.insert(0, str(_agreement_python_root())) from hkp_pack import agreement, descriptor_context # noqa: E402 +from hkp_pack import generic_targets as gtmod # noqa: E402 from hkp_pack.errors import HkpPackError # noqa: E402 @@ -208,6 +209,17 @@ def _specialization_twins(order: list, by_label: dict, knobs: set) -> list: return violations +def _shared_devices(left: list, right: list, table) -> str: + """The devices both competing arch lists admit.""" + left_devices = gtmod.expand(left, table) + right_devices = gtmod.expand(right, table) + if left_devices is None or right_devices is None: + shared = left_devices or right_devices + else: + shared = left_devices & right_devices + return ", ".join(sorted(shared)) if shared else "every arch" + + def effective_arch( bundles: list[descriptor_context.Bundle], requested: str | None ) -> str: @@ -232,8 +244,9 @@ class Payloads: descriptor that cannot produce them has not shown its evidence is about the artifact it ships, so that is a failure rather than an unchecked property.""" - def __init__(self, kpack_python_dir: str | None = None): + def __init__(self, kpack_python_dir: str | None = None, generic_targets=None): self._dir = kpack_python_dir + self._table = generic_targets self._archives: dict = {} self._module = None @@ -269,8 +282,12 @@ def read(self, entry: descriptor_context.Entry, arch: str) -> bytes: f"{entry.ukd.get('name')}: kernel_source.library resolves to " f"{archive_path}, which does not exist." ) + # A generic document's archive is keyed by the generic's name. + if self._table is None: + self._table = gtmod.GenericTargets.load(gtmod.DEFAULT_TABLE_PATH) + key = descriptor_context.archive_arch(entry.arch, arch, self._table) try: - blob = self._archive(archive_path).get_kernel(toc_key, arch) + blob = self._archive(archive_path).get_kernel(toc_key, key) except HkpPackError: raise except Exception as exc: @@ -280,7 +297,7 @@ def read(self, entry: descriptor_context.Entry, arch: str) -> bytes: if blob is None: raise GateError( f"{entry.ukd.get('name')}: {archive_path} carries no member " - f"{toc_key!r} for {arch}." + f"{toc_key!r} for {key}." ) return blob @@ -303,6 +320,7 @@ def check( mode: str, arch: str | None = None, payloads: Payloads | None = None, + generic_targets=None, *, provenance_root: str | None = None, ): @@ -316,6 +334,8 @@ def check( `provenance_root` is as for `descriptor_context.Index`. """ + if generic_targets is None: + generic_targets = gtmod.GenericTargets.load(gtmod.DEFAULT_TABLE_PATH) index = descriptor_context.Index(root, provenance_root=provenance_root) schemas = index.schemas() all_bundles = descriptor_context.resolve_bundles(index) @@ -357,16 +377,11 @@ def check( collisions = [] for i, (left, left_key) in enumerate(completed): for right, right_key in completed[i + 1 :]: - if left_key != right_key or not agreement.overlap(left.arch, right.arch): + if left_key != right_key or not gtmod.compete( + left.arch, right.arch, generic_targets + ): continue - # A wildcard covers whatever the other side names, so the overlap it - # reports is that side's list rather than an empty intersection. - both = set(left.arch) & set(right.arch) - either = set(left.arch) | set(right.arch) - where = ( - ", ".join(sorted(both if left.arch and right.arch else either)) - or "every arch" - ) + where = _shared_devices(left.arch, right.arch, generic_targets) collisions.append( f"{left.ukd.get('name')} and {right.ukd.get('name')} complete to one " f"tuple on {where}" @@ -482,7 +497,9 @@ def check( # producing compile's evidence says what the binary was built with. Full # mode checks it against the payload bytes; structural mode names it. if mode == "full": - records = descriptor_context.consumer_records(all_bundles, schemas, arch) + records = descriptor_context.consumer_records( + all_bundles, schemas, arch, generic_targets + ) for bundle in bundles: for entry in bundle.entries: name = entry.ukd.get("name") @@ -614,6 +631,11 @@ def main(argv=None) -> int: "bytes a packed descriptor names under --mode full. Omit it to use the " "installed one.", ) + parser.add_argument( + "--generic-targets-json", + default=str(gtmod.DEFAULT_TABLE_PATH), + help="The generic GPU target table (gpu_generic_targets.json).", + ) parser.add_argument( "--provenance-root", action="append", @@ -652,8 +674,13 @@ def main(argv=None) -> int: sets, by_label, bad, skipped, knobs = {}, {}, [], [], set() unverified: list[str] = [] - payloads = Payloads(args.kpack_python_dir) if args.mode == "full" else None try: + generic_targets = gtmod.GenericTargets.load(args.generic_targets_json) + payloads = ( + Payloads(args.kpack_python_dir, generic_targets) + if args.mode == "full" + else None + ) arch = None if args.mode == "full": probe = descriptor_context.resolve_bundles( @@ -670,6 +697,7 @@ def main(argv=None) -> int: args.mode, arch, payloads, + generic_targets, provenance_root=provenance_roots.get(label), ) sets[label] = binaries diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md index 33058eba4396..bc0d537d98bc 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/RUNBOOK.md @@ -166,7 +166,7 @@ descriptor index expands as the recursive glob `/**/*.json` — a walk of the wh filesystem. Review the finalized inventory after deduplication. Resolve KDP → UED → KMD by UUID; -tuple identity includes schema types/defaults and effective architecture overlap. +tuple identity includes schema types/defaults and the arch tier at which kernels compete. Structural mode reports compiled agreement as `NOT CHECKED` and can exit 0 with unrun checks. `--profile` optionally selects the bundle and supplies vocabulary, never compiler evidence. Full artifact checking belongs after packing. diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md index 86262dfabc38..e5d5dd3948f4 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/SKILL.md @@ -41,9 +41,7 @@ AMDGPU code object resolved relative to the descriptor that names it (inside the root, no root-relative fallback) and `symbol` is authored. The packer copies the bytes as-is into the arch's archive with no compile step, reads the signature from the object's metadata, and declares `metadata_fields: []`. It does not check the object's -format or target processor, so every `hsaco` kernel must state a non-empty `arch` listing the arch(es) its -object runs on (a generic-target object lists each one); the generator rejects a kernel -without one, since an unrestricted one would ship the same bytes to every shard. `hsaco_file` (direct load) remains unsupported. +format or target processor; `arch` follows [hsaco kernels and `arch`](../../../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#hsaco-kernels-and-arch). `hsaco_file` (direct load) remains unsupported. Both dialects are packed and staged per architecture, and **neither registers a descriptor in CMake**: the authored subpath is the whole mechanism. Only diff --git a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md index 55208e0c33cf..1d32c41c16e8 100644 --- a/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md +++ b/projects/hipdnn/tools/ai/skills/hipdnn-ingestor-engine/extend.md @@ -12,8 +12,8 @@ KMD whose filename does not follow the individual packs. Preserve old names, UUIDs, symbols, references and native hooks; only genuinely new objects get new IDs. Compare completed, KMD-typed metadata across the whole engine: -conflicting tuples on overlapping effective architectures fail, equal tuples on disjoint -architectures are legal. New topology, layouts, attributes or feature combinations +equal tuples fail only when they compete at the same arch tier on a device; disjoint +architectures or different tiers (explicit over generic over empty) are legal. New topology, layouts, attributes or feature combinations reopen [graph-contract.md](graph-contract.md), even for a "variant". A new variant can be one added KDP entry or a standalone UKD reference, with no new @@ -68,7 +68,7 @@ The one registration a generated engine needs is the **kernel source**, and only | Group | Kinds | What happens | |---|---|---| | Compilation inputs | `hip`, `rocke` | Authored, lowered to a code object at pack time. No CMake registration. | -| Prebuilt input | `hsaco` | Authored as `{file, symbol}`, packed byte-for-byte: no compile, no CMake registration. `arch` is mandatory: list every arch the object runs on (a generic-target object lists all of them), on the kernel or inherited from its pack. | +| Prebuilt input | `hsaco` | Authored as `{file, symbol}`, packed byte-for-byte: no compile, no CMake registration. `arch` is mandatory; see [hsaco kernels and `arch`](../../../../../../dnn-providers/hip-kernel-provider/descriptor-packaging/README.md#hsaco-kernels-and-arch). | | Passthrough | `embedded_source` | Shipped as authored, no producer runs, no archive entry. **Needs the registration below.** | | Runtime descriptor form | `kpack` | Written *by* the packer and shipped; the runtime sees only this. The packer's intermediate also uses `kind: hsaco` for compiled variants. |