diff --git a/src/domain/gpu/gpuCodeObjects.ts b/src/domain/gpu/gpuCodeObjects.ts index af216fa0a9..5f8bd69026 100644 --- a/src/domain/gpu/gpuCodeObjects.ts +++ b/src/domain/gpu/gpuCodeObjects.ts @@ -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, [ diff --git a/src/gpu/GpuElfKernels.ts b/src/gpu/GpuElfKernels.ts index 1e4e1c5976..9bf8f4acc3 100644 --- a/src/gpu/GpuElfKernels.ts +++ b/src/gpu/GpuElfKernels.ts @@ -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(), - 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 { diff --git a/src/gpu/GpuInventory.ts b/src/gpu/GpuInventory.ts index f3286d87ec..f7870c095c 100644 --- a/src/gpu/GpuInventory.ts +++ b/src/gpu/GpuInventory.ts @@ -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) { diff --git a/src/gpu/GpuObject.ts b/src/gpu/GpuObject.ts index 84aeb98947..75935fbd74 100644 --- a/src/gpu/GpuObject.ts +++ b/src/gpu/GpuObject.ts @@ -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", diff --git a/tests/acceptance/gpuCodeObjectsCli.test.ts b/tests/acceptance/gpuCodeObjectsCli.test.ts index 652219d235..d8cc4a9733 100644 --- a/tests/acceptance/gpuCodeObjectsCli.test.ts +++ b/tests/acceptance/gpuCodeObjectsCli.test.ts @@ -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"); +}); diff --git a/tests/fixtures/gpu/codeObjects.ts b/tests/fixtures/gpu/codeObjects.ts index e09be6509b..d4b711a296 100644 --- a/tests/fixtures/gpu/codeObjects.ts +++ b/tests/fixtures/gpu/codeObjects.ts @@ -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, [