fix(gpu): retain AMD target declaration evidence

This commit is contained in:
morluto
2026-10-27 08:55:43 +08:00
parent aa253bd13f
commit b2df588b4a
6 changed files with 274 additions and 16 deletions
+62
View File
@@ -35,6 +35,26 @@ export const gpuObjectSchema = z.strictObject({
location: gpuLocationSchema,
})
.nullable(),
amd_target_metadata: z
.strictObject({
flags: z.string().regex(/^0x[0-9a-f]{8}$/u),
processor: z.string().nullable(),
header_location: gpuLocationSchema,
note_coverage: z
.enum(["complete", "partial"])
.describe(
"Complete only after all declared ELF note sections were examined within supported AMD metadata coverage; partial retains only examined facts.",
),
notes: z.array(
z.strictObject({
target: z.string().nullable(),
target_state: z.enum(["absent", "declared", "invalid"]),
location: gpuLocationSchema,
}),
),
})
.nullable()
.default(null),
target_consistency: z.enum([
"unexamined",
"single-source",
@@ -207,6 +227,48 @@ export const gpuCodeObjectsSchema = gpuCodeObjectsObjectSchema.superRefine(
const row = report.objects[index]!;
range(row.location, ["objects", index, "location"]);
range(row.metadata_location, ["objects", index, "metadata_location"]);
const amd = row.amd_target_metadata;
if (amd !== null) {
range(amd.header_location, [
"objects",
index,
"amd_target_metadata",
"header_location",
]);
if (
row.kind !== "amdgpu" ||
amd.header_location.container_id !== row.id ||
amd.header_location.offset !== 0 ||
![52, 64].includes(amd.header_location.size)
)
issue(
["objects", index, "amd_target_metadata"],
"AMDGPU header provenance must start at its payload",
);
for (let note = 0; note < amd.notes.length; note++) {
const location = amd.notes[note]!.location;
range(location, [
"objects",
index,
"amd_target_metadata",
"notes",
note,
"location",
]);
if (location.container_id !== row.id)
issue(
[
"objects",
index,
"amd_target_metadata",
"notes",
note,
"location",
],
"AMDGPU notes must use their payload coordinate space",
);
}
}
const header = row.cuda_target_header;
if (header !== null) {
range(header.location, [
+57 -14
View File
@@ -369,6 +369,7 @@ const cudaKernels = async (
export interface AmdElfMetadata {
readonly target: string | null;
readonly targetIdentity: AmdGpuTargetIdentity | null;
readonly targetMetadata: import("../domain/gpu/gpuCodeObjects.js").GpuObject["amd_target_metadata"];
readonly limitations: string[];
readonly notes: {
section: NativeSection;
@@ -388,12 +389,18 @@ export const readAmdElfMetadata = async (
{ kind: "incomplete-amd-metadata"; cause: unknown; partial: AmdElfMetadata }
>
> => {
const targets = new Set<string>(),
limitations: string[] = [];
const limitations: string[] = [];
const notes: AmdElfMetadata["notes"] = [];
let processor: string | null = null;
let retainedBytes = 0;
let unsupportedNotes = false;
let targetMetadata: AmdElfMetadata["targetMetadata"] = null;
const observed = (): AmdElfMetadata => {
const targets = new Set(
(targetMetadata?.notes ?? []).flatMap(({ target }) =>
target === null ? [] : [target],
),
);
const processor = targetMetadata?.processor ?? null;
const noteTarget = targets.size === 1 ? [...targets][0]! : null;
const noteIdentity =
noteTarget === null ? null : readAmdGpuNoteTarget(noteTarget);
@@ -402,12 +409,17 @@ export const readAmdElfMetadata = async (
processor !== null &&
noteIdentity !== null &&
processor !== noteIdentity.processor;
const conflict = noteConflict || processorConflict;
const limitation = noteConflict
? "AMDGPU notes declare conflicting processor targets; processor identity is unknown"
: processorConflict
? "AMDGPU ELF processor flags conflict with target note metadata; processor identity is unknown"
: null;
const invalidTarget = (targetMetadata?.notes ?? []).some(
({ target_state }) => target_state === "invalid",
);
const conflict = noteConflict || processorConflict || invalidTarget;
const limitation = invalidTarget
? "AMDGPU note target is not a string; processor identity is unknown"
: noteConflict
? "AMDGPU notes declare conflicting processor targets; processor identity is unknown"
: processorConflict
? "AMDGPU ELF processor flags conflict with target note metadata; processor identity is unknown"
: null;
if (limitation !== null && !limitations.includes(limitation))
limitations.push(limitation);
const target = conflict ? null : (noteTarget ?? processor);
@@ -418,12 +430,25 @@ export const readAmdElfMetadata = async (
(noteTarget === null && processor !== null
? { key: processor, processor }
: null));
return { target, targetIdentity, limitations, notes };
return { target, targetIdentity, targetMetadata, limitations, notes };
};
try {
if (directory.machine !== 224)
rejectGpu("AMDGPU metadata requires an AMDGPU ELF machine");
processor = readAmdGpuElfProcessor(directory.flags);
targetMetadata = {
flags: `0x${directory.flags.toString(16).padStart(8, "0")}`,
processor: readAmdGpuElfProcessor(directory.flags),
header_location: {
container_id: source.containerId,
offset: 0,
size: directory.bits === 64 ? 64 : 52,
section_index: null,
section_name: null,
},
note_coverage: "partial",
notes: [],
};
for (const section of directory.sections) {
if (section.type !== 7) continue;
const sectionSize = gpuCount(section.size);
@@ -458,19 +483,36 @@ export const readAmdElfMetadata = async (
const metadata = amdRecord(
readAmdMetadata(bytes.subarray(desc, desc + size)),
);
if (metadata === null) rejectGpu("AMDGPU metadata is not a map");
const target = amdString(metadata?.["amdhsa.target"]);
const target_state = !Object.hasOwn(metadata!, "amdhsa.target")
? "absent"
: target === null
? "invalid"
: "declared";
if (target_state === "invalid") unsupportedNotes = true;
targetMetadata.notes.push({
target,
target_state,
location: site(source, section, desc, size),
});
const kernels = metadata?.["amdhsa.kernels"];
if (!Array.isArray(kernels))
return rejectGpu("AMDGPU metadata has no valid kernel array");
const target = amdString(metadata!["amdhsa.target"]);
if (target !== null) targets.add(target);
notes.push({ section, noteOffset: p, desc, size, kernels });
} else if (vendor === "AMD" && (type === 10 || type === 11))
} else if (vendor === "AMD" && (type === 10 || type === 11)) {
unsupportedNotes = true;
limitations.push(
"Legacy AMDGPU YAML metadata is outside MessagePack coverage",
);
}
p = end;
}
}
targetMetadata.note_coverage =
unsupportedNotes || directory.sections.length === 0
? "partial"
: "complete";
if (notes.length === 0)
throw new BinaryFormatFailure(
"unsupported",
@@ -491,6 +533,7 @@ const amdKernels = (
onTarget?.({
value: prepared.target,
identity: prepared.targetIdentity,
amdMetadata: prepared.targetMetadata,
scope: "amdgpu",
});
for (const {
+3
View File
@@ -455,11 +455,13 @@ export class GpuInventory {
this.#retain({
payload_target: payload.value,
cuda_target_header: payload.cudaHeader ?? null,
amd_target_metadata: payload.amdMetadata ?? null,
target_consistency: state,
target: selectedTarget ?? payload.value,
});
row.payload_target = payload.value;
row.cuda_target_header = payload.cudaHeader ?? null;
row.amd_target_metadata = payload.amdMetadata ?? null;
row.target_consistency = state;
if (selectedTarget === null) row.target = payload.value;
if (row.target === null) this.discovery = "partial";
@@ -513,6 +515,7 @@ export class GpuInventory {
this.#target(row, {
value: prepared.target,
identity: prepared.targetIdentity,
amdMetadata: prepared.targetMetadata,
scope: "amdgpu",
});
if (!inspected.ok) {
+2
View File
@@ -3,6 +3,7 @@ import type { GpuSource } from "./GpuSource.js";
import type { AmdGpuTargetIdentity } from "./AmdGpuTarget.js";
export type GpuPayloadTarget = {
readonly value: string | null;
readonly amdMetadata?: GpuObject["amd_target_metadata"];
readonly cudaHeader?: GpuObject["cuda_target_header"];
readonly limitation?: string;
} & (
@@ -38,6 +39,7 @@ export const gpuObject = (
target: null,
payload_target: null,
cuda_target_header: null,
amd_target_metadata: null,
target_consistency: "unexamined",
bundle_id: null,
compression: "none",
+148
View File
@@ -20,6 +20,7 @@ import {
ccobFixture,
ptxFixture,
amdFixture,
amdNoteFixture,
cubinFixture,
hipFixture,
elfFixture,
@@ -1186,3 +1187,150 @@ cliTest(
});
},
);
cliTest(
"retains AMD target declarations and examined note coverage through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-amd-target-sources-");
const { call } = await connectLocalToolsMcp();
const first = "amdgcn-amd-amdhsa--gfx900";
const second = "amdgcn-amd-amdhsa--gfx1100";
const legacyNote = Buffer.alloc(20);
legacyNote.writeUInt32LE(4, 0);
legacyNote.writeUInt32LE(4, 4);
legacyNote.writeUInt32LE(10, 8);
legacyNote.write("AMD\0", 12);
const headerConflict = amdFixture();
headerConflict.writeUInt32LE(0x141, 48);
for (const [name, bytes, declarations, coverage, effective] of [
[
"note-conflict",
amdFixture(false, amdNoteFixture(false, second)),
[first, second],
"complete",
null,
],
["header-conflict", headerConflict, [first], "complete", null],
[
"duplicate",
amdFixture(false, amdNoteFixture()),
[first, first],
"complete",
first,
],
["legacy-tail", amdFixture(false, legacyNote), [first], "partial", first],
[
"invalid-target",
amdFixture(false, undefined, 123),
[null],
"partial",
null,
],
[
"absent-target",
amdFixture(false, undefined, null),
[null],
"complete",
"gfx900",
],
[
"truncated-tail",
amdFixture(false, Buffer.alloc(3)),
[first],
"partial",
first,
],
] as const) {
const path = join(root, name);
await writeFile(path, bytes);
const response = await cli.run({
arguments: [
"inspect-gpu-code-objects",
path,
"--analysis",
"kernels",
"--detail",
"complete",
"--json",
],
});
expect(response.exitCode, response.stderr).toBe(0);
const reply = await call("inspect_gpu_code_objects", {
path,
analysis: "kernels",
detail: "complete",
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
for (const value of [response.json, reply.structuredContent]) {
const report = gpuCodeObjectsSchema.parse(
parseEvidence(value).normalized_result,
);
const row = report.objects[0]!;
expect(row).toMatchObject({
target: effective,
payload_target: effective,
});
const metadata = row.amd_target_metadata!;
expect(metadata).toMatchObject({
flags: name === "header-conflict" ? "0x00000141" : "0x0000012c",
processor: name === "header-conflict" ? "gfx1100" : "gfx900",
header_location: { container_id: row.id, offset: 0, size: 64 },
note_coverage: coverage,
});
expect(metadata.notes.map(({ target }) => target)).toEqual(
declarations,
);
expect(
new Set(metadata.notes.map(({ location }) => location.offset)).size,
).toBe(declarations.length);
for (const note of metadata.notes) {
expect(note.location.container_id).toBe(row.id);
expect(note.location.section_name).toBe(".note");
const source = bytes.subarray(
note.location.offset,
note.location.offset + note.location.size,
);
if (note.target !== null)
expect(source.includes(Buffer.from(note.target))).toBe(true);
expect(note.target_state).toBe(
name === "invalid-target"
? "invalid"
: name === "absent-target"
? "absent"
: "declared",
);
}
expect(report.kernels).toHaveLength(declarations.length);
if (coverage === "partial")
expect(report.coverage.payloads).toBe("partial");
}
}
},
);
it("does not interpret non-AMD ELF flags as AMD processor declarations through MCP", async () => {
const root = await createTestTempDirectory("rea-amd-machine-");
const path = join(root, "wrong-machine");
const payload = amdFixture();
payload.writeUInt16LE(62, 18);
await writeFile(path, hipFixture(payload, "hip-amdgcn-amd-amdhsa--"));
const { call } = await connectLocalToolsMcp();
const reply = await call("inspect_gpu_code_objects", {
path,
analysis: "kernels",
detail: "complete",
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
const report = gpuCodeObjectsSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
expect(report.objects.find(({ kind }) => kind === "amdgpu")).toMatchObject({
target: null,
payload_target: null,
amd_target_metadata: null,
target_consistency: "unknown",
inspection: "malformed",
});
expect(report.kernels).toEqual([]);
expect(report.coverage.payloads).toBe("partial");
});
+2 -2
View File
@@ -213,7 +213,7 @@ export const cubinFixture = (
/** One producer-shaped AMDGPU MessagePack note, composable with later notes. */
export const amdNoteFixture = (
hidden = false,
target: string | null = "amdgcn-amd-amdhsa--gfx900",
target: string | number | null = "amdgcn-amd-amdhsa--gfx900",
): Buffer => {
const args = [
{ ".offset": 0, ".size": 8, ".value_kind": "global_buffer" },
@@ -252,7 +252,7 @@ export const amdNoteFixture = (
export const amdFixture = (
hidden = false,
trailingNote?: Buffer,
target: string | null = "amdgcn-amd-amdhsa--gfx900",
target: string | number | null = "amdgcn-amd-amdhsa--gfx900",
): Buffer => {
const note = amdNoteFixture(hidden, target);
return elfFixture(224, [