refactor(gpu): retain parsed AMD target identities

Carry exact target keys and canonical processor facts from Clang bundle and ELF metadata producers into inventory agreement checks. Remove the string-only handoff and downstream reparsing while keeping public spelling, selection, uncertainty and admission behavior.

Cover unrecognized targets, conflicting notes and identity retained before malformed notes through the provider boundary. Preserve distinct unselected-payload behavior for standalone ELF and known bundle targets.
This commit is contained in:
morluto committed 2026-10-24 08:37:15 +08:00
1 parent 9d4293be27
commit 3c0b26df67
7 files changed
+208 -52

No files matched your search

+11 -8
View File
@@ -78,10 +78,13 @@ const ELF_PROCESSORS: Readonly<Record<number, string>> = {
0xeb: "gfx1250-strict",
};
export interface AmdGpuTargetIdentity {
readonly key: string;
readonly processor: string;
}
/** Retain exact target spelling separately from its canonical processor identity. */
export const parseAmdGpuTargetId = (
value: string,
): { readonly key: string; readonly processor: string } | null => {
const parseAmdGpuTargetId = (value: string): AmdGpuTargetIdentity | null => {
const match = targetId.exec(value);
return match?.[1] === undefined
? null
@@ -92,18 +95,18 @@ export const parseAmdGpuTargetId = (
export const isAmdGpuBundleId = (name: string): boolean =>
bundleTriple.test(name);
export const readAmdGpuBundleTarget = (name: string): string | null => {
export const readAmdGpuBundleTarget = (
name: string,
): AmdGpuTargetIdentity | null => {
if (!isAmdGpuBundleId(name)) return null;
const target = bundleTarget.exec(name)?.[1];
return target === undefined
? null
: (parseAmdGpuTargetId(target)?.key ?? null);
return target === undefined ? null : parseAmdGpuTargetId(target);
};
/** Preserve the full target ID separately from its processor identity. */
export const readAmdGpuNoteTarget = (
target: string,
): { readonly key: string; readonly processor: string } | null => {
): AmdGpuTargetIdentity | null => {
const value = noteTarget.exec(target)?.[1];
return value === undefined ? null : parseAmdGpuTargetId(value);
};
+21 -7
View File
@@ -13,14 +13,21 @@ import {
type GpuSource,
} from "./GpuSource.js";
import { gpuObject } from "./GpuObject.js";
import { isAmdGpuBundleId, readAmdGpuBundleTarget } from "./AmdGpuTarget.js";
import {
isAmdGpuBundleId,
readAmdGpuBundleTarget,
type AmdGpuTargetIdentity,
} from "./AmdGpuTarget.js";
export const CLANG_BUNDLE_MAGIC = Buffer.from("__CLANG_OFFLOAD_BUNDLE__");
export const CCOB_MAGIC = Buffer.from("CCOB");
export const readClangBundleDirectory = async (
source: GpuSource,
base: number,
signal?: AbortSignal,
): Promise<{ objects: GpuObject[]; end: number }> => {
): Promise<{
declarations: { object: GpuObject; amdTarget: AmdGpuTargetIdentity | null }[];
end: number;
}> => {
const header = await source.read(base, 32),
count = gpuCount(header.readBigUInt64LE(24));
if (!header.subarray(0, 24).equals(CLANG_BUNDLE_MAGIC))
@@ -31,7 +38,13 @@ export const readClangBundleDirectory = async (
65536,
"HIP bundle entry count exceeds the inventory budget",
);
const objects: GpuObject[] = [];
// One wrapper/identity per admitted entry (at most 65536). Processor text is
// no longer than its admitted name; cumulative name bytes remain
// within the 16 MiB directory bound before identities are allocated.
const declarations: {
object: GpuObject;
amdTarget: AmdGpuTargetIdentity | null;
}[] = [];
let position = base + 32;
for (let index = 0; index < count; index++) {
if (index % 256 === 0) {
@@ -71,18 +84,19 @@ export const readClangBundleDirectory = async (
metadata: { offset: position, size: 24 + nameSize },
});
row.bundle_id = name;
row.target = readAmdGpuBundleTarget(name);
objects.push(row);
const amdTarget = readAmdGpuBundleTarget(name);
row.target = amdTarget?.key ?? null;
declarations.push({ object: row, amdTarget });
position += 24 + nameSize;
}
let end = position;
for (const row of objects) {
for (const { object: row } of declarations) {
if (row.encoded_size === 0) continue;
if (row.location.offset < position)
rejectGpu("HIP entry overlaps its bundle directory");
end = Math.max(end, row.location.offset + row.encoded_size);
}
return { objects, end };
return { declarations, end };
};
export const readCcobHeader = (bytes: Buffer, remaining: number) => {
if (bytes.length < 20) rejectGpu("CCOB header is truncated");
+10 -6
View File
@@ -1,5 +1,6 @@
import { readCudaTarget } from "./CudaTarget.js";
import {
type AmdGpuTargetIdentity,
readAmdGpuElfProcessor,
readAmdGpuNoteTarget,
} from "./AmdGpuTarget.js";
@@ -359,7 +360,7 @@ const cudaKernels = async (
};
export interface AmdElfMetadata {
readonly target: string | null;
readonly target_key: string | null;
readonly targetIdentity: AmdGpuTargetIdentity | null;
readonly limitations: string[];
readonly notes: {
section: NativeSection;
@@ -403,10 +404,13 @@ export const readAmdElfMetadata = async (
limitations.push(limitation);
const target = conflict ? null : (noteTarget ?? processor);
// LLVM notes use both the empty and explicit `unknown` environment forms.
const target_key = conflict
const targetIdentity = conflict
? null
: (noteIdentity?.key ?? (noteTarget === null ? processor : null));
return { target, target_key, limitations, notes };
: (noteIdentity ??
(noteTarget === null && processor !== null
? { key: processor, processor }
: null));
return { target, targetIdentity, limitations, notes };
};
try {
if (directory.machine !== 224)
@@ -476,8 +480,8 @@ const amdKernels = (
): { limitations: string[]; coverage: "complete" | "partial" } => {
onTarget?.({
value: prepared.target,
key: prepared.target_key,
scope: "exact",
identity: prepared.targetIdentity,
scope: "amdgpu",
});
for (const {
section,
+34 -22
View File
@@ -52,7 +52,7 @@ import {
readPtxKernels,
readPtxTarget,
} from "./PtxKernels.js";
import { parseAmdGpuTargetId } from "./AmdGpuTarget.js";
import type { AmdGpuTargetIdentity } from "./AmdGpuTarget.js";
/** File declarations only. Every child range stays in its named parent coordinate space. */
export class GpuInventory {
@@ -68,7 +68,10 @@ export class GpuInventory {
unknown_section_names = 0;
#factBytes = 0;
#workBytes = 0;
readonly #outerTargets = new WeakMap<GpuObject, string | null>();
readonly #outerTargets = new WeakMap<
GpuObject,
string | AmdGpuTargetIdentity | null
>();
readonly #selectedObjects: ReadonlySet<string> | undefined;
readonly #selectedTargets: ReadonlySet<string> | undefined;
constructor(
@@ -88,9 +91,9 @@ export class GpuInventory {
throw new GpuReportCapacityFailure();
this.#factBytes += bytes + 2;
}
#add(row: GpuObject): void {
#add(row: GpuObject, amdTarget?: AmdGpuTargetIdentity | null): void {
if (row.raw_flags !== null || row.bundle_id !== null)
this.#outerTargets.set(row, row.target);
this.#outerTargets.set(row, amdTarget ?? row.target);
this.#retain(row);
this.#retain(" ".repeat(32)); // Reserve all later inspection-state changes.
this.objects.push(row);
@@ -417,28 +420,37 @@ export class GpuInventory {
}
#target(row: GpuObject, payload: GpuPayloadTarget): string | undefined {
const selectedTarget = row.target;
const outer = this.#outerTargets.get(row) ?? null;
const outerIdentity = this.#outerTargets.get(row) ?? null;
const outer =
typeof outerIdentity === "string"
? outerIdentity
: (outerIdentity?.key ?? null);
const payloadKey =
payload.scope === "amdgpu"
? (payload.identity?.key ?? null)
: payload.key;
const hasOuter = outer !== null;
let state: GpuObject["target_consistency"] = "unknown";
if (payload.value !== null && !hasOuter) state = "single-source";
else if (outer !== null && payload.key !== null) {
else if (outer !== null && payloadKey !== null) {
if (payload.scope === "cuda-base") {
const base = /^sm_([0-9]+)[a-z]*$/u.exec(outer)?.[1];
const inner = /^sm_([0-9]+)$/u.exec(payload.key)?.[1];
const inner = /^sm_([0-9]+)$/u.exec(payloadKey)?.[1];
if (base !== undefined && inner !== undefined)
state = base === inner ? "base-only" : "conflict";
} else if (ptxTargetIdentity(outer) === ptxTargetIdentity(payload.key))
} else if (payload.scope === "amdgpu") {
if (outer === payloadKey) state = "consistent";
else
state =
typeof outerIdentity === "object" &&
outerIdentity !== null &&
payload.identity !== null &&
outerIdentity.processor === payload.identity.processor
? "unknown"
: "conflict";
} else if (ptxTargetIdentity(outer) === ptxTargetIdentity(payloadKey))
state = "consistent";
else if (row.kind === "amdgpu") {
const outerTarget = parseAmdGpuTargetId(outer);
const payloadTarget = parseAmdGpuTargetId(payload.key);
state =
outerTarget !== null &&
payloadTarget !== null &&
outerTarget.processor === payloadTarget.processor
? "unknown"
: "conflict";
} else state = "conflict";
else state = "conflict";
}
this.#retain({
payload_target: payload.value,
@@ -500,8 +512,8 @@ export class GpuInventory {
if (prepared.limitations.length > 0) this.discovery = "partial";
this.#target(row, {
value: prepared.target,
key: prepared.target_key,
scope: "exact",
identity: prepared.targetIdentity,
scope: "amdgpu",
});
if (!inspected.ok) {
this.discovery = "partial";
@@ -566,8 +578,8 @@ export class GpuInventory {
base,
this.signal,
);
for (const row of directory.objects) {
this.#add(row);
for (const { object: row, amdTarget } of directory.declarations) {
this.#add(row, amdTarget);
if (row.kind === "host") row.inspection = "complete";
else if (row.kind === "amdgpu" && row.target === null)
await this.#amdPayload(source, row);
+6 -4
View File
@@ -1,12 +1,14 @@
import type { GpuLocation, GpuObject } from "../domain/gpu/gpuCodeObjects.js";
import type { GpuSource } from "./GpuSource.js";
export interface GpuPayloadTarget {
import type { AmdGpuTargetIdentity } from "./AmdGpuTarget.js";
export type GpuPayloadTarget = {
readonly value: string | null;
readonly key: string | null;
readonly scope: "exact" | "cuda-base";
readonly cudaHeader?: GpuObject["cuda_target_header"];
readonly limitation?: string;
}
} & (
| { readonly scope: "exact" | "cuda-base"; readonly key: string | null }
| { readonly scope: "amdgpu"; readonly identity: AmdGpuTargetIdentity | null }
);
export const gpuLocation = (
source: GpuSource,
offset: number,
+116 -3
View File
@@ -16,6 +16,7 @@ import {
elfFixture,
ptxFixture,
amdFixture,
amdNoteFixture,
llvmBitcodeFixture,
} from "../../fixtures/gpu/codeObjects.js";
const inspect = async (
@@ -363,6 +364,74 @@ it.each(["amdgcn", "amdgpu9.00"])(
expect(result.value.kernels).toHaveLength(1);
},
);
it.each([
"producer-specific-gpu-target",
"amdgpu9.00-amd-amdhsa--gfx900:xnack",
])(
"retains the unrecognized AMDGPU note target %s without substituting ELF processor flags",
async (noteTarget) => {
const result = await inspect(
hipFixture(amdFixture(false, undefined, noteTarget)),
{ targets: ["gfx900"] },
);
if (!result.ok) throw result.error;
const row = result.value.objects.find(({ kind }) => kind === "amdgpu");
expect(row).toMatchObject({
target: "gfx900",
payload_target: noteTarget,
target_consistency: "unknown",
inspection: "complete",
});
expect(result.value.coverage.payloads).toBe("complete");
expect(result.value.kernels).toMatchObject([
{ name: "add_one", parameter_bytes: 12, registers: { sgpr: 6, vgpr: 2 } },
]);
expect(row?.limitations.join(" ")).not.toContain(
"Conflicting target declarations",
);
expect(result.value.kernels[0]?.limitations.join(" ")).not.toContain(
"Conflicting target declarations",
);
},
);
it("retains kernels from conflicting AMDGPU notes without choosing either note or ELF flags", async () => {
const bytes = amdFixture(
false,
amdNoteFixture(false, "amdgcn-amd-amdhsa--gfx1100"),
);
const result = await inspect(bytes);
if (!result.ok) throw result.error;
expect(result.value.objects).toMatchObject([
{
kind: "amdgpu",
target: null,
payload_target: null,
target_consistency: "unknown",
inspection: "partial",
},
]);
expect(result.value.objects[0]?.limitations).toContain(
"AMDGPU notes declare conflicting processor targets; processor identity is unknown",
);
expect(result.value.coverage).toMatchObject({
discovery: "partial",
payloads: "partial",
});
expect(result.value.kernels).toMatchObject([
{ name: "add_one", parameter_bytes: 12, registers: { sgpr: 6, vgpr: 2 } },
{ name: "add_one", parameter_bytes: 12, registers: { sgpr: 6, vgpr: 2 } },
]);
expect(new Set(result.value.kernels.map(({ id }) => id)).size).toBe(2);
const selected = await inspect(bytes, { targets: ["gfx900"] });
if (!selected.ok) throw selected.error;
expect(selected.value.objects[0]).toMatchObject({
target: null,
payload_target: null,
target_consistency: "unknown",
inspection: "not-selected",
});
expect(selected.value.kernels).toEqual([]);
});
it.each(
[
"amdgcn-amd-amdhsa-unknown-",
@@ -791,9 +860,18 @@ it.each(["truncated", "invalid", "extension"] as const)(
tail.write("AMDGPU\0", 12);
tail[20] = kind === "invalid" ? 0xc1 : 0xd4;
}
for (const bytes of [
amdFixture(false, tail),
hipFixture(amdFixture(false, tail)),
const payloadTarget = "amdgcn-amd-amdhsa--gfx900";
for (const { bytes, target, consistency } of [
{
bytes: amdFixture(false, tail),
target: payloadTarget,
consistency: "single-source",
},
{
bytes: hipFixture(amdFixture(false, tail)),
target: "gfx900",
consistency: "consistent",
},
]) {
const result = await inspect(bytes);
if (!result.ok) throw result.error;
@@ -806,7 +884,42 @@ it.each(["truncated", "invalid", "extension"] as const)(
expect(
result.value.objects.find((row) => row.kind === "amdgpu")!.inspection,
).toBe(kind === "extension" ? "unsupported" : "malformed");
const identity = {
target,
payload_target: payloadTarget,
target_consistency: consistency,
};
expect(
result.value.objects.find((row) => row.kind === "amdgpu"),
).toMatchObject(identity);
expect(result.value.coverage.payloads).toBe("partial");
const selected = await inspect(bytes, { targets: [target] });
if (!selected.ok) throw selected.error;
expect(
selected.value.objects.find((row) => row.kind === "amdgpu"),
).toMatchObject(identity);
expect(selected.value.kernels).toEqual(result.value.kernels);
expect(selected.value.coverage.payloads).toBe("partial");
const skipped = await inspect(bytes, {
targets: [target === "gfx900" ? payloadTarget : "gfx900"],
});
if (!skipped.ok) throw skipped.error;
expect(
skipped.value.objects.find((row) => row.kind === "amdgpu"),
).toMatchObject(
target === payloadTarget
? {
...identity,
inspection: kind === "extension" ? "unsupported" : "malformed",
}
: {
target,
payload_target: null,
target_consistency: "unexamined",
inspection: "not-selected",
},
);
expect(skipped.value.kernels).toEqual([]);
}
},
);
+10 -2
View File
@@ -210,9 +210,9 @@ export const cubinFixture = (
}
return result;
};
export const amdFixture = (
/** One producer-shaped AMDGPU MessagePack note, composable with later notes. */
export const amdNoteFixture = (
hidden = false,
trailingNote?: Buffer,
target: string | null = "amdgcn-amd-amdhsa--gfx900",
): Buffer => {
const args = [
@@ -247,6 +247,14 @@ export const amdFixture = (
note.writeUInt32LE(32, 8);
note.write("AMDGPU\0", 12);
metadata.copy(note, 20);
return note;
};
export const amdFixture = (
hidden = false,
trailingNote?: Buffer,
target: string | null = "amdgcn-amd-amdhsa--gfx900",
): Buffer => {
const note = amdNoteFixture(hidden, target);
return elfFixture(224, [
{
name: ".note",