CUSTOM_OP Wire Byte-Layout
All symbols, addresses, and byte offsets on this page apply to
neuronx_cc2.24.5133.0+58f8de22 (cp310 wheel; cp310/11/12 share an identicallibwalrus.so.text, with a fixed per-build address delta). The encoder, validators, and bounded field-setters all live inlibwalrus.so(file offset == VA; PT_LOAD 1:1). The BIR nodebir::InstCustomOpand theModuleArtifactInforegistry live inlibBIR.so(linked intolibwalrus.so). Evidence anchor: D-Q08 (mined from theCoreV2GenImpl::visitInstCustomOpbody and thecore_v2custom-op validators). Treat every address as version-pinned. See Build & Version Provenance.
Abstract
A bir::InstCustomOp — the compiler's escape hatch for an arbitrary user kernel that runs on the 8-core Xtensa GPSIMD CPU cluster — does not encode to a single 64-byte ISA word. It encodes to a chain of them: one CUSTOM_OP_HEADER (opcode 0x85) followed by one or more CUSTOM_OP_PAYLOAD words (opcode 0x86), one payload per operand access pattern. The header carries the counts and the kernel identity (a CustomOpFunctionId byte plus num_payloads and num_arguments); each payload carries one operand's access pattern. The first payload is always the output AP; the remaining N payloads are the N input/argument APs, in order. The runtime reads num_payloads off the header to know how many 0x86 words to consume before the next instruction.
The two opcodes share the entire 64-byte-bundle skeleton with every other Tonga ISA instruction — pxor-zeroed in full, header-stamped by setupHeader (so byte[0]=opcode, byte[1]=0x10, giving the 16-bit instruction word 0x1085/0x1086), a shared per-instruction header-init band, then the family-specific fields, then fwrite(buf, 1, 0x40, bin). The lifecycle is documented in 2.1 The 64-Byte Instruction Bundle; this page maps only the custom-op-specific bytes: the header's three count/id fields and the payload's present-flag + ADDR4 access-pattern record.
The bar for this page: a reader can byte-encode a CUSTOM_OP_HEADER / CUSTOM_OP_PAYLOAD pair by hand — knowing each field's offset, width, value source, and the exact num_payloads = N+2 arithmetic — and reconstruct the full word chain for a custom op with one output and N inputs. Every offset is pinned against a literal store in the visitInstCustomOp body (the LOBYTE/LOWORD opcode stamps, the lea [r13+0Ch] / lea [r13+0Fh] bounded setters that stamp num_payloads and num_arguments) or against the core_v2 validator that re-checks it.
For reimplementation, the contract is:
- The two-opcode chain model:
0x85header once, then0x86payload per operand AP (output first, thenNargs) — not a single self-describing word. - The header byte map:
num_payloads(u16 @+0x0C),CustomOpFunctionId(u8 @+0x0E),num_arguments(u8 @+0x0F), zero @+0x10— a contiguous count-band at+0x0C..+0x10. - The
num_payloads = N + 2arithmetic and what the+2accounts for. - The payload byte map: present-flag (
1@+0x0F) + one ADDR4-rooted access-pattern descriptor @+0x10.
| Sole encoder | neuronxcc::backend::CoreV2GenImpl::visitInstCustomOp @ 0x12613c0 |
| Header opcode | 0x85 (133) → 16-bit word 0x1085 (4229) |
| Payload opcode | 0x86 (134) → 16-bit word 0x1086 (4230) |
| Bundle size | std::array<u8,64>; inst_word_len = 0x10 (16 dwords) |
| Words per op | 1 + num_payloads; num_payloads = N + 2 (N = num_arguments) |
| Header validators | core_v2::is_valid_custom_op_header @ 0x129b940; dbg_… @ 0x129ba90 |
| Payload validator | core_v2::dbg_is_valid_custom_op_payload @ 0x129c610 |
| Arch gate | birverifier::checkCustomOp @ 0xfdbf90 — Sunda(20) ∨ Tonga(10) only |
| Kernel identity | CustomOpFunctionId (u8) → ModuleArtifactInfo lib/function side-table |
| Evidence | D-Q08 (visitInstCustomOp body + core_v2 validators) |
NOTE — There is exactly one encoder.
nm -DCfinds noCoreV3/CoreV4override ofvisitInstCustomOp; the gen3/gen4 backends inherit theCoreV2base path verbatim. Custom-op encoding is arch-shared. (The op is gated to Sunda/Tonga arch by a separate verifier, but the wire bytes are produced by the single CoreV2 emitter.)
1. The chain model — header once, payload per operand
A custom op is the only TPB instruction that emits a variable-length chain of 64-byte words. The shape is fixed by the operand count N (= num_arguments, the length of the InstCustomOp argument list at inst+0xA0..+0xA8):
CUSTOM_OP_HEADER (0x85) ── counts + FunctionId, no AP
CUSTOM_OP_PAYLOAD (0x86) ── OUTPUT access pattern (payload 0)
CUSTOM_OP_PAYLOAD (0x86) ── INPUT arg[0] access pattern (payload 1)
CUSTOM_OP_PAYLOAD (0x86) ── INPUT arg[1] access pattern (payload 2)
… …
CUSTOM_OP_PAYLOAD (0x86) ── INPUT arg[N-1] access pattern (payload N)
── next instruction ──
Total 64-byte words on the wire = 1 (header) + (N + 1) (payloads) = N + 2. The header's num_payloads field is set to N + 2 (§2.2). The decoder reads num_payloads off the 0x85 header, then consumes that many 0x86 words before resuming the instruction stream.
The emitter's per-argument loop walks the InstCustomOp argument list and emits one 0x86 word each:
/* CoreV2GenImpl::visitInstCustomOp @0x12613c0 — chain skeleton (annotated C) */
void visitInstCustomOp(InstCustomOp &inst) {
int N = arg_list_length(inst); /* inst+0xA0..+0xA8 (arg vector) */
/* ---- one CUSTOM_OP_HEADER (0x85) ---- */
array<u8,64> hdr = {0}; /* emplace_back + pxor-zero */
setupHeader(&hdr, /*opcode=*/0x85); /* hdr[0]=0x85, hdr[1]=0x10 */
customop_header_init(&hdr, inst); /* sub_122ED00 — shared band */
set_u16(&hdr[0x0C],/*name=*/"instr.num_payloads", N ? N + 2 : 1); /* sub_123CA60: lea [r13+0Ch] */
hdr[0x0E] = function_id; /* CustomOpFunctionId (u8) */
set_u8 (&hdr[0x0F],/*name=*/"instr.num_arguments", N); /* sub_1247BF0 */
hdr[0x10] = 0;
emit(&hdr); /* fwrite 0x40 + COLLECT 133 */
/* ---- one CUSTOM_OP_PAYLOAD (0x86) per operand: output first, then args ---- */
array<u8,64> p0 = {0};
setupHeader(&p0, /*opcode=*/0x86);
p0[0x0F] = 1; /* present flag */
encode_access_pattern(&p0[0x10], getOutput_AP(inst, 0)); /* sub_1210900: out AP */
emit(&p0); /* fwrite 0x40 + COLLECT 134 */
for (int i = 0; i < N; ++i) {
array<u8,64> pi = {0};
setupHeader(&pi, /*opcode=*/0x86);
pi[0x0F] = 1;
encode_access_pattern(&pi[0x10], getArgument_AP(inst, i)); /* sub_1210900 */
if (i == N - 1) customop_payload_terminator(&pi, inst); /* sub_122EA40 */
emit(&pi);
}
}
GOTCHA — the kernel name and
.soare NOT on the wire. The ISA stream is name-free. The header carries only the numericCustomOpFunctionId(byte+0x0E); the human-readable kernel/library name and thelibbuiltincustomop_cpu{0..7}.stripped.so(or user.so) reference are registered separately intobir::ModuleArtifactInfo(addCustomOpLibFile/addCustomOpFunction/allocateCustomOpFunctionId), serialized into the NEFF metadata, and resolved by the runtime loader against the FunctionId. The FunctionId↔(libfile, function) binding is the subject of 11.7 FindCustomOpData Staging & FunctionId Binding. STRONG (D-Q08 — theregisterCustomOp-tagged registration path and theinst+0x110/+0xF0/+0x130/…string reads).
2. The CUSTOM_OP_HEADER (0x85) byte map
The header is the only word that carries the op's shape. Its custom-op-specific payload is three small fields plus a zero byte, sitting on top of the shared bundle header (+0x00..+0x03) and the shared per-instruction init band written by customop_header_init (sub_122ED00).
| off | Sz | field | value | source / store | Conf |
|---|---|---|---|---|---|
+0x00 | 1 | opcode = 0x85 (133) | const | setupHeader; LOBYTE(hdr[0]) = -123 | CONFIRMED |
+0x01 | 1 | inst_word_len = 0x10 | const | setupHeader a2[1]=16 → word 0x1085 | CONFIRMED |
+0x02 | 2 | reserved = 0 | const | setupHeader / pxor-zero | CONFIRMED |
+0x04 | 8 | header-init band (engine / sync / neuron-events) | per-inst | customop_header_init (sub_122ED00) | STRONG |
+0x0C | 2 | num_payloads (u16) = N + 2 | computed | set_u16(hdr+0x0C, …) sub_123CA60 "instr.num_payloads"; dest lea [r13+0Ch] @0x1262f75 | CONFIRMED |
+0x0E | 1 | CustomOpFunctionId (u8) | registry | allocateCustomOpFunctionId() → hdr[14] | CONFIRMED |
+0x0F | 1 | num_arguments (u8) = N | arg count | set_u8(hdr+15, …) sub_1247BF0 "instr.num_arguments" | CONFIRMED |
+0x10 | 1 | reserved = 0 | const | explicit hdr[16] = 0 | CONFIRMED |
+0x11..+0x3F | 47 | reserved = 0 | const | pxor-zero (no store) | INFERRED |
2.1 The contiguous +0x0C..+0x10 count-band
NOTE —
num_payloads,FunctionId, andnum_argumentsform a contiguous count-band at+0x0C..+0x10, clear of the header-init band. Every other TPB instruction treats+0x04..+0x0Bas a generic 8-byteSyncInfo/ header overlay; the custom-op header leaves that band intact and writes its three count/id fields immediately after it. The bounded u16 settersub_123CA60stampsnum_payloadsathdr+0x0C(destlea rdi,[r13+0Ch], machine bytes49 8d 7d 0c@0x1262f75), thenCustomOpFunctionIdat+0x0Eand the bounded u8 setter at+0x0F(lea rdi,[r13+0Fh],49 8d 7d 0f@0x1262fbe) follow in order. Because+0x0Cis past+0x0B, none of these fields overwrites the init band — a reimplementer runs the shared header init, then stamps the count-band, with no ordering hazard between them. CONFIRMED —sub_123CA60is theutils.h:292bounded setter (width0x10, raises ifvalue != (u16)value) and its target ishdr+0x0C; the field-name string"instr.num_payloads"is verbatim.rodata.
CORRECTION (D-Q08) —
num_payloadsis at+0x0C, not+0x06. An earlier draft of this page placednum_payloadsat header+0x06. That was a decompiler-misread: Hex-Rays renders the store assub_123CA60((_WORD*)hdr + 6, "instr.num_payloads", …), andhdris typed_WORD*(u16), so+ 6is u16-pointer arithmetic = 6 × 2 = +0x0C bytes, not a+6-byte offset. The byte-proven truth is+0x0C: the setter's destination islea rdi,[r13+0Ch](machine bytes49 8d 7d 0c) at 0x1262f75 in the encoder body (visitInstCustomOp@0x12613c0), with the COLLECT scratch arm at 0x1263201 stamping the same[…+0Ch]. The cross-check that proves it: the sibling fieldnum_argumentsis stored via a genuine_BYTE*+ 15→lea [r13+0Fh]→ byte+0x0F— by the sibling u8 settersub_1247BF0(call @0x1262fcf), different pointer type and width from the u16sub_123CA60(num_payloads, @0x1262f86), which is exactly what flipped onlynum_payloads. The sibling 2.22 Collective/GPSIMD/CustomOp Encoding §10 map (which placednum_payloadsat+0x0C) is CORRECT; this page now agrees with it.
2.2 The num_payloads = N + 2 arithmetic — what the +2 is
num_payloads is not the number of arguments. It is the number of 0x86 payload words that follow the header:
num_payloads = N + 2
= 1 (output AP payload) ← payload 0, always present
+ N (input/argument AP payloads)
+ 1 (header word counted in the total) ← see note
The cleanest decomposition consistent with the chain (§1) and the emitter's N ? N+2 : 1 rule:
Nargument payloads — one0x86per input.+1for the output access pattern (payload 0, always emitted even for a single-output op).+1for the header word itself —num_payloadsis the total chunk count of the instruction (header + all payloads), so a downstream chunk-reader advances exactlynum_payloads64-byte words to reach the next instruction.
NOTE — the
else 1branch. WhenN == 0(a custom op with no input arguments), the emitter setsnum_payloads = 1, not0 + 2 = 2. This is the degenerate "header-only-ish" count for the no-argument case (the chunk-walker still needs a non-zero advance). The(#args + 2) if args present, else 1rule is read directly from the emitter; the exact semantics of the zero-argument / scalar-fill path (whether an output payload is still emitted whenN==0) is the one OPEN edge in D-Q08 — treatnum_payloads = N + 2as the rule for the commonN ≥ 1case and1forN = 0. CONFIRMED (theN+2/1values); INFERRED (the precise per-chunk decomposition of the+2, since the runtime chunk-walker is not inlibwalrus).
GOTCHA —
num_argumentscounts inputs only; the output is separate.num_arguments(byte+0x0F) = the length of theInstCustomOpargument list (inputs). The single output is not counted innum_arguments— it rides payload 0. So a 3-input / 1-output op hasnum_arguments = 3andnum_payloads = 5(3 inputs + 1 output + 1 header). A reimplementer must keep the two counters distinct:num_arguments = N,num_payloads = N + 2. CONFIRMED — the emitter counts arguments (inst+0xA0..+0xA8list) and the single output separately; the"Custom ops cannot have more than 1 output"legality check (.rodata) confirms the at-most-one-output invariant.
3. The CUSTOM_OP_PAYLOAD (0x86) byte map
Each payload is a near-empty bundle carrying a present-flag and one access-pattern descriptor. The same 0x86 layout is reused for the output AP (payload 0) and for every input arg AP — only the AP source differs.
| off | Sz | field | value | source / store | Conf |
|---|---|---|---|---|---|
+0x00 | 1 | opcode = 0x86 (134) | const | setupHeader; LOBYTE(pl[0]) = -122 | CONFIRMED |
+0x01 | 1 | inst_word_len = 0x10 | const | setupHeader a2[1]=16 → word 0x1086 | CONFIRMED |
+0x02 | 2 | reserved = 0 | const | setupHeader / pxor-zero | CONFIRMED |
+0x04 | 8 | header-init band | per-inst | shared header overlay | STRONG |
+0x0F | 1 | present flag = 1 | const | explicit pl[0x0F] = 1 | CONFIRMED |
+0x10 | 48 | access-pattern descriptor (ADDR4-rooted) | operand AP | encode_access_pattern(pl+0x10, …) sub_1210900 | CONFIRMED (store); STRONG (AP internals) |
3.1 The present flag
The byte at +0x0F is stamped to 1 on every payload. It marks the payload slot as occupied — the consumer treats +0x0F == 1 as "this 0x86 word carries a valid AP". The payload validator dbg_is_valid_custom_op_payload (0x129c610) branches on byte0 == 0x86 and validates the descriptor/event fields; the present flag is the per-payload occupancy marker. CONFIRMED — the pl[0x0F] = 1 store is in the emit path of every payload arm.
NOTE — the present flag occupies the same byte offset (
+0x0F) that the header uses fornum_arguments. This is not a collision: they are different opcodes (0x85vs0x86) with independent field meanings at+0x0F. In the header,+0x0F=num_arguments(u8 count); in the payload,+0x0F= present flag (1). A decoder dispatches onbyte[0]first, then interprets+0x0Fper-opcode.
3.2 The access-pattern descriptor — ADDR4-rooted, at +0x10
The payload's body (+0x10 onward, up to 48 bytes) is one operand access pattern, encoded by the generic AP packer sub_1210900. The AP is the same on-chip tensor memory-pattern descriptor the rest of the ISA uses: it opens with a 32-bit ADDR4 start-address word (see 2.2 ADDR4 — the 32-Bit Address Word), followed by the per-dimension loop words (TENSOR1D/2D/3D/4D shape, depending on the operand rank). The ADDR4 word at +0x10 packs the operand's SBUF/HBM byte address, its mode nibble, and (for a register-mode AP) a colored-register id.
- Payload 0 encodes the output AP:
getOutput<AccessPattern>(inst, 0). - Payload
i+1(fori ∈ [0, N)) encodes thei-th input AP:getArgument<AccessPattern>(inst, i).
/* CUSTOM_OP_PAYLOAD body @ +0x10 — one AP descriptor (sub_1210900) */
encode_access_pattern(&payload[0x10], operand_AP);
/* payload[0x10..0x13] = ADDR4 word (start addr; see 2.2 addr4.md) */
/* payload[0x14..] = TENSORnD loop words (per-dim num/step; see 2.3) */
GOTCHA — a custom-op operand AP cannot be a
TensorIndirect(gather) AP. The emitter raises"Hardware Restriction: CUSTOM_OP instruction cannot have TensorIndirect AP"(.rodata). So the ADDR4 word at+0x10is always a static or register-mode address (mode nibble0b00or RM=1) — never the indirect-gather mode (0x20). All operands must additionally be located in SBUF or HBM ("All args to a customop must be located in SBUF or HBM","All of a customop's outputs must be located in SBUF or HBM"). CONFIRMED — the three legality strings are verbatim in the emitter's.rodata.
NOTE — the AP descriptor internals are out of scope here. The 48-byte AP layout (ADDR4 + the
TENSORnDloop words, the per-dtype alignment, the SBUF/PSUM region split) is the subject of 2.2 ADDR4 and 2.3 Tensor Descriptors. This page pins only that the payload's AP starts at+0x10and is ADDR4-rooted; the per-field bit map of the AP itself is deferred to those pages. D-Q08 leaves the per-field AP descriptor ofsub_1210900OPEN (the AP-per-payload structure and the output-first ordering are confirmed; the internal offsets of the descriptor are the AP pages' subject).
4. Worked byte-encoding example — one output, one input (N = 1)
Encode a custom op with CustomOpFunctionId = 0x07, one input argument and one output, both small SBUF tiles. N = num_arguments = 1, so num_payloads = N + 2 = 3, and the chain is three 64-byte words: header, output payload, input payload.
WORD 0 — CUSTOM_OP_HEADER (0x85), 64 bytes, little-endian:
off : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
val : 85 10 00 00 [HH HH HH HH HH HH HH HH] 03 00 07 01
│ │ └──┘ └──── header-init band ────┘ │ │ │ │
│ │ resvd (sub_122ED00 fills +04..+0B,│ │ │ └ num_arguments = 0x01 (N=1)
│ │ left intact — count-band │ │ └─── CustomOpFunctionId = 0x07
│ │ starts past it at +0x0C) │ └────── num_payloads hi = 0x00
│ └ inst_word_len = 0x10 (word = 0x1085) └───────── num_payloads lo = 0x03
└─── opcode = 0x85 (133)
+0C..0D (num_payloads, u16 LE) = 03 00 ← N + 2 = 3 ⟵ count-band, clear of the init band
+0E (CustomOpFunctionId) = 07
+0F (num_arguments) = 01
+10 = 00 ; +11..3F = 00 (pxor-zero)
WORD 1 — CUSTOM_OP_PAYLOAD (0x86), the OUTPUT AP:
off : 00 01 02 03 04..0B 0F 10 11 12 13 14..
val : 86 10 00 00 …… … 01 <ADDR4 out> <TENSORnD loop words…>
│ │ │
│ │ └ present flag = 1 (byte +0x0F)
│ └ word = 0x1086
└─── opcode = 0x86 (134)
+10..13 = ADDR4(output tile) (start addr; mode 0b00 static or RM=1)
+14.. = TENSORnD loop words for the output operand
WORD 2 — CUSTOM_OP_PAYLOAD (0x86), the INPUT arg[0] AP:
off : 00 01 02 03 … 0F 10 11 12 13 14..
val : 86 10 00 00 … 01 <ADDR4 in0> <TENSORnD loop words…>
+10..13 = ADDR4(input arg[0] tile)
+14.. = TENSORnD loop words for arg[0]
The runtime: reads byte[0]=0x85 → custom-op header; reads num_payloads=3 @ +0x0C → consume 2 more 0x86 words (payload 0 = output, payload 1 = arg[0]); resolves CustomOpFunctionId=0x07 against the NEFF ModuleArtifactInfo registry to find the kernel .so and entry function; dispatches the kernel on the GPSIMD Xtensa cluster with the output and input tiles addressed by the two payload APs.
NOTE — bytes marked
HHare header-init-band content, not free. The+0x04..+0x0Bband is filled bycustomop_header_init(sub_122ED00) with the shared engine/sync/neuron-events fields, and the count-band that follows (num_payloads@+0x0C,FunctionId@+0x0E,num_arguments@+0x0F, zero @+0x10) sits entirely past it — no init-band byte is overwritten by a count-band stamp. For a clean reimplementation, run the shared header init over+0x04..+0x0B, then stampnum_payloads,FunctionId,num_arguments, and the+0x10zero into the contiguous+0x0C..+0x10band. The validators (is_valid_custom_op_header) only assertbyte1 == 0x10,byte0 == 0x85,LOWORD == 0x1085, valid neuron-events, and high descriptor bits zero — they do not constrain the+0x04..+0x0Binit bytes.
5. Validation — what the round-trip checker requires
The RUN_ISA_CHECKS codegen mode re-encodes the same words onto scratch and runs a vtable-dispatched checker. The custom-op checkers:
/* core_v2::is_valid_custom_op_header @0x129b940 */
bool is_valid_custom_op_header(word) {
require(BYTE1(word) == 0x10); /* inst_word_len high byte */
require(is_valid_enum(4, byte0)); /* opcode is a known opcode-class-4 */
require(byte0 == 0x85); /* the header opcode */
require(has_valid_neuron_events(…)); /* the header-init events band */
require(high_descriptor_bits == 0);
/* canonical LOWORD = 0x1085 = 4229 */
}
/* core_v2::dbg_is_valid_custom_op_payload @0x129c610 */
bool dbg_is_valid_custom_op_payload(word) {
branch(byte0 == 0x86); /* the payload opcode */
validate_payload_descriptor_and_events(…);
}
| Checker | Address | Asserts | Conf |
|---|---|---|---|
is_valid_custom_op_header | 0x129b940 | byte0==0x85, byte1==0x10, LOWORD==0x1085, valid events | CONFIRMED |
dbg_is_valid_custom_op_header | 0x129ba90 | mirror (debug build) | CONFIRMED |
dbg_is_valid_custom_op_payload | 0x129c610 | byte0==0x86, descriptor/events | CONFIRMED |
These wire-validators — the full set, the codegen-mode plumbing, and the RUN_ISA_CHECKS vs GENERATE_ISACODE vs COLLECT_OPCODES three-way switch — are the subject of 11.6 Custom-Op Wire Validators. This page documents only the two custom-op-specific checkers and the byte assertions they pin.
NOTE — three codegen modes, one encoder.
visitInstCustomOpswitches on the codegen mode (gen+0x270): GENERATE_ISACODE (mode 1) emplaces thearray<u8,64>, stamps, encodes, andfwrites0x40bytes to the engine.bin; RUN_ISA_CHECKS (mode 2) encodes on scratch and runs the validators with nofwrite; COLLECT_OPCODES (mode 0) inserts the opcode int (133header /134payload) into the per-instruction opcode set and bumps amap<u32,u32>histogram. Any other mode raises"Wrong CodeGenMode. It must be one of GENERATE_ISACODE, RUN_ISA_CHECKS, or COLLECT_OPCODES". The byte stamps are identical across GENERATE and RUN_ISA_CHECKS — the validator checks the same bytes the generator writes. CONFIRMED (D-Q08).
6. Legality constraints the emitter enforces
The encoder raises (reportError) on these conditions, all verbatim .rodata in the visitInstCustomOp body:
| Constraint | Error string | Conf |
|---|---|---|
| At most one output | "Custom ops cannot have more than 1 output" | CONFIRMED |
| Inputs in SBUF/HBM | "All args to a customop must be located in SBUF or HBM" | CONFIRMED |
| Outputs in SBUF/HBM | "All of a customop's outputs must be located in SBUF or HBM" | CONFIRMED |
| No gather AP | "Hardware Restriction: CUSTOM_OP instruction cannot have TensorIndirect AP" | CONFIRMED |
| ≤ 254 unique functions | "Number of unique custom op functions cannot exceed " (cap 0xFE = 254) | CONFIRMED |
| Arch is Sunda/Tonga | "Arch == ArchLevel::Sunda || Arch == ArchLevel::Tonga" (checkCustomOp @0xfdbf90) | CONFIRMED |
GOTCHA —
CustomOpFunctionIdis a u8, capped at 254 unique functions (0xFF= unset).allocateCustomOpFunctionId()hands out a denseuint32id that is truncated to a byte at the wire; theModuleArtifactInfofunction-count guard (+456) raises"Number of unique custom op functions cannot exceed "once the unique-function count would exceed0xFE(254). The id is a single byte (hdr+0x0E) with0xFFreserved as the "unset" sentinel, so a NEFF can reference at most 254 distinct custom-op functions. The id-to-(libfile, function-name) map is NEFF metadata, resolved by the runtime — the wire only carries the byte. CONFIRMED — the u8 width ofhdr[0x0E]and the0xFEcount guard pin the 254 usable-id ceiling.CORRECTION (#826 reconciliation) — the cap is
0xFE= 254, not 255, and the error string carries no baked "255". An earlier draft of this page stated a 255 cap and quoted"…cannot exceed 255". The.rodataliteral is the prefix"Number of unique custom op functions cannot exceed "with no number compiled in, and the guard is0xFE(254 usable ids,0xFF= unset). This now agrees with 11.7, 11.10 §4B, and the upstream 6.5.8 emitter analysis.
GOTCHA — the GPSIMD here is the Xtensa CPU cluster, NOT the Pool-alias "GPSIMD" mover. The
0x85/0x86pair dispatches a kernel onto the programmable 8-core Xtensa GPSIMD CPU array (11.1 The GPSIMD CPUs: 8-core Xtensa ELF Layout). That is a different subsystem from theInstGPSIMDSB2SB(0xBF) Pool-alias cross-core SBUF↔SBUF mover documented in 2.22 §5 and 1.13 GPSIMD Engine. The three-way "GPSIMD" name collision is resolved in full on the 1.13 page; the custom-op pair on this page is the Xtensa-cluster dispatch, and SORT (and arbitrary user NKI kernels) are its canonical occupants (11.2 Bitonic SORT/TOPK).
7. Confidence ledger
- CONFIRMED (byte-store / opcode / validator disassembled in the
visitInstCustomOpbody and thecore_v2checkers): both opcodes (0x85=133 /0x86=134, fromLOBYTE = -123 / -122, COLLECT records 133/134, validatorsv12==-123 / v10==-122); the0x1085/0x108616-bit words (setupHeader a2[1]=16, validatorLOWORD=4229);num_payloadsu16 @+0x0C(sub_123CA60, destlea [r13+0Ch]49 8d 7d 0c@0x1262f75,"instr.num_payloads");CustomOpFunctionIdu8 @+0x0E;num_argumentsu8 @+0x0F(sub_1247BF0, destlea [r13+0Fh]49 8d 7d 0f@0x1262fbe,"instr.num_arguments"); thehdr[0x10]=0store; the payload present flag=1@+0x0F; the AP-per-payload structure (output viagetOutput, args viagetArgument, encoded bysub_1210900@+0x10); thenum_payloads = N+2 | 1rule; all six legality strings + the 254-function u8 cap (0xFEguard,0xFFunset); the Sunda/Tonga arch gate. - STRONG: the
+0x04..+0x0Bheader-init band sub-layout (shared overlay written bysub_122ED00, not re-decoded here); the AP descriptor being ADDR4-rooted (cross-referenced to 2.2 ADDR4, not independently store-pinned in this body); the FunctionId↔registry binding path (registerCustomOptag + string reads, the subject of 11.7). - INFERRED / DEFER: the precise per-chunk decomposition of the
+2(header vs output vs the runtime chunk-walker's advance semantics, since the walker is not inlibwalrus); theN == 0zero-argument edge (num_payloads = 1); the 48-byte AP descriptor internals written bysub_1210900(2.2 ADDR4, 2.3 Tensor Descriptors); the+0x0C..+0x0Dinit-band byte meanings.
The encoder body is authoritative for every offset cited. No SPECULATIVE claims.
Cross-References
- 2.22 Collective / GPSIMD / CustomOp Encoding — the sibling ISA-encoding page; its §10 first mapped the
0x85/0x86pair and correctly placednum_payloadsat+0x0C; this page gives the full byte-encoding contract and agrees on+0x0C. - 2.1 The 64-Byte Instruction Bundle & Header Skeleton — the
pxor-zero /setupHeader/fwrite(0x40)lifecycle every word here shares. - 2.2 ADDR4 — the 32-Bit Address Word — the access-pattern start word that opens every payload's operand descriptor at
+0x10. - 2.3 Tensor Descriptors — the
TENSORnDloop words that follow the ADDR4 word in the payload body. - 11.1 The GPSIMD CPUs: 8-core Xtensa ELF Layout — the Xtensa CPU cluster these
0x85/0x86words dispatch a kernel onto. - 11.2 The Bitonic SORT / TOPK Builtin Algorithm — SORT, the canonical custom-op occupant (no dedicated opcode; rides
0x85/0x86). - 11.6 Custom-Op Wire Validators — the full
is_valid_custom_op_*checker set and the codegen-mode plumbing. - 11.7 FindCustomOpData Staging & FunctionId Binding — how the header's FunctionId byte maps to a
(libfile, function-name)viaModuleArtifactInfo. - 1.13 GPSIMD Engine — the Pool-Alias Cross-Core SB2SB Mover — the other "GPSIMD" (the
0xBFPool-alias mover); resolves the three-way name collision.