Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The bir::Instruction Base Struct

All addresses on this page apply to neuronx_cc 2.24.5133.0+58f8de22, the cp310 build of libBIR.so (md5 12bb979f7ca41248252abb0f16b2da98, sha256 2387d0d4…dc5a4cfa). The cp311/cp312 builds carry the identical ABI but their virtual addresses drift; offsets and ordinals are version-stable.

Abstract

bir::Instruction is the single polymorphic root of the BIR (Backend IR) — the L1 instruction representation that libBIR.so defines, the simulator interprets, and the birverifier checks. Every one of the 110 InstructionType opcodes is an Inst<Name> class that derives from bir::Instruction and only appends its own operands above the shared header; the base bytes are byte-identical across all opcodes. This page reconstructs that base object in full — from the vtable pointer at offset 0, through the inline header, to the heap-allocated scheduling/dependency block hung off +0xD0 — and enumerates the 19 JSON keys the common header serializes.

The object is physically split in two. Part (A) is the inline header, +0x00 .. +0xD8: the vptr, the bir::NamedObject CRTP sub-object (name / origin / optin-mask / unique-id), the InstructionType tag at +0x58, the loopnest and predicate vectors, the engine assignment, the three intrusive operand lists, and a single pointer at +0xD0. Part (B) is a separately heap-allocated scheduling/dependency block of 0xA98 (2712) bytes — "Storage/DepInfo" — that holds the dependency edge lists, the symbolic schedule order, the resolved schedule cycles, the sync info, and the debug provenance. The constructor operator news the block and stores it at +0xD0; no opcode overrides this split.

BIR is not built on LLVM's Value/User model, but it borrows two LLVM data structures wholesale: the operand lists are llvm::ilist-style intrusive circular sentinels, and each dependency edge is an llvm::PointerIntPair<Instruction*, 3> that packs the target pointer and a 3-bit EdgeKind tag into one word. A reimplementer who knows those two structures and the table below can rebuild the node.

The page is structured as: the inline header layout (§1), the +0xD0 block layout (§2), the constructor as annotated pseudocode (§3), the 19-key JSON header (§4), the dependency-edge model (§5), the subclass graph and the 113-vs-401 reconciliation (§6), and the shared vtable (§7).

Reimplementation contract

To rebuild a BIR node a reimplementer must reproduce:

  • The two-part object split: an inline header ending near +0xD8 plus a 0xA98-byte heap block referenced at +0xD0. Opcodes append fields starting near +0xF0.
  • The InstructionType tag at +0x58 as the single dispatch key. Visitor dispatch, equality (sameInst), the JSON factory, and the verifier all switch on this int — there is no C++ accept() virtual.
  • The three intrusive operand sentinels at +0xA0/+0xB0/+0xC0 (ins / indirection_ins / outs), self-initialized empty.
  • The dependency model: a PointerIntPair<Instruction*,3> edge node, with normal dependencies and loop-carried dependencies as separate list heads in the block but FLOW/ANTI/OUTPUT/ORDERED kinds distinguished by the 3-bit tag on each node, not separate heads.
  • The 19-key JSON header emitted by Instruction::toJson, in order, with the per-key emit guards.
Defining binarylibBIR.so (cp310), 22151 functions
Base ctorbir::Instruction::Instruction(string&, BasicBlock*, InstructionType) @ 0x2dafb0
Common JSON emitbir::Instruction::toJson(json&) @ 0x2e2ea0
Inline header size~0xD8 bytes (first op field near +0xF0)
+0xD0 block size0xA98 (2712) bytes, heap, operator new in ctor
IT tag offset+0x58 (InstructionType, 0..109)
vtable_ZTVN3bir11InstructionE @ 0x8fced8; vptr = symbol +0x10 = 0x8fcee8
Opcodes110 InstructionType values; 6 are abstract bases
Polymorphic Inst classes113 distinct _ZTSN3bir…Inst* typeinfo strings
Source path (asserts)neuronxcc/walrus/ir/lib/IR/Instruction.cpp

NOTE — IDA's structures.json for libBIR.so contains only ELF/libc types — no bir::* struct. The layout below is reconstructed from the constructor's stores and the accessor/serializer reads, every offset anchored to one of them. There is no struct definition in the binary to copy; this table is the definition.


1. The Inline Header — +0x00 .. +0xD8

All offsets are byte offsets into this. Each row is pinned to a constructor store (ctor @ 0x2dafb0, §3) and, where one exists, a named accessor or the toJson read.

FieldOffsetTypeMeaning / JSON keyAnchorConf
vptr+0x00void**vtable ptr (_ZTVN3bir11InstructionE + 0x10); slot +0x60 = getValidEnginesctor *this = off_8FCEE8CERTAIN
(NamedObject hdr)+0x08qword[2]NamedObjectBase holder slot (zeroed)ctor *(OWORD*)(this+24)-1 = 0HIGH
name.data+0x18char*std::string name data ptr (SSO → this+0x28)ctor this+0x18 = this+0x28CERTAIN
name.size+0x20qwordname lengthctor (sub_2D3970)CERTAIN
name.sso+0x28char[16]SSO buffer (cap 15)ctorCERTAIN
origin+0x38int (NamedObjectOrigin)"origin"{Internal,Penguin,NKI}ctor *(this+0x38) = 0; toJson [a1+56]CERTAIN
optin_passes_mask+0x40qword (bitset)"optin_passes" bitmaskctor *(this+0x40) = 15; optinPassesToJsonHIGH
unique_id+0x48intunique object id (-1 sentinel → getUniqueId)ctor *(this+0x48) = -1 then = UniqueIdCERTAIN
parent+0x50bir::BasicBlock*owning basic block (a3)ctor *(this+0x50) = a3CERTAIN
IT tag+0x58int (InstructionType)"opcode" (via InstructionType2string)ctor *(this+0x58) = a4; toJson [a1+88]CERTAIN
(pad)+0x5Cintalignment padHIGH
loopnest+0x60vector<LoopAxis*>"loopnest" — begin/end/capctor OWORD@+0x60 = 0; getLoopnest = this+0x60CERTAIN
predicates+0x78vector<AffinePredicate>"predicates" (record stride 40)ctor OWORD@+0x70/+0x80 = 0CERTAIN
engine+0x90int (EngineType)"engine" (current engine assignment)ctor qword@+0x90 = 0; setEngineId idx36; toJson *(p)+144CERTAIN
engine_id+0x94uint32"engine_id" (per-engine instance index)setEngineId writes idx37; toJson *(p)+148CERTAIN
(scratch)+0x98int (=0)reservedctor *(this+0x98) = 0HIGH
ins HEAD+0xA0circular-dlist {next,prev}"ins"InstructionArgumentType 0ctor movddup self@+0xA0; toJson this+160CERTAIN
indirection_ins HEAD+0xB0circular-dlist {next,prev}"indirection_ins" — IAT 1ctor movddup self@+0xB0; toJson this+176CERTAIN
outs HEAD+0xC0circular-dlist {next,prev}"outs" — IAT 2ctor movddup self@+0xC0; toJson this+192CERTAIN
block ptr+0xD0Storage/DepInfo*pointer to the 0xA98 heap block (§2)ctor *(this+0xD0) = new(0xA98)CERTAIN
(tail scratch)+0xD8qword[3] = 0ctor zeroes +0xD8/+0xE0/+0xE8; op fields followctorHIGH

The base header is opcode-independent. The first op-specific field begins after +0xD8: the matmul family's first MaybeAffine is at +0xF0, InstDMA's DGEType at +0xF8. So +0xD8 .. +0xEF is a subclass-dependent gap before the per-op payload.

NOTE — engine/engine_id (+0x90/+0x94) are base fields inherited by all 110 opcodes, not InstDMA fields as their DMA-centric naming suggests. setEngineId (@ 0x2d6ab0) and the base toJson both operate on them at the base offsets; InstDMA's own first field is DGEType @ +0xF8.

The NamedObject CRTP sub-object (+0x08 .. +0x4F)

bir::Instruction is itself a bir::NamedObject<bir::Instruction, bir::BasicBlock> (CRTP), whose typeinfo string _ZTSN3bir11NamedObjectINS_11InstructionENS_10BasicBlockEEE is present in the binary. This sub-object supplies the name std::string (+0x18, SSO-inlined into +0x28), the origin enum (+0x38), the optin_passes bitmask (+0x40), and the unique_id (+0x48). The ctor calls bir::NamedObject<…>::getUniqueId(this+8) to allocate the id, and — a self-consistency check baked into every construction — throws "Id has already been initialzed" (sic) if +0x48 is not still -1 at that point.

GOTCHA — the optin_passes default of 15 (ctor mov [this+0x40], 15) is read as the optin bitmask by optinPassesToJson, but it has not been cross-checked against an optin-pass writer; treat the specific value 15 as HIGH, not CERTAIN.

The operand sentinels (+0xA0 / +0xB0 / +0xC0)

Each of the three operand lists is an llvm::ilist-style intrusive circular doubly-linked sentinel: a {next, prev} head node that the ctor seeds with movddup xmm, &head so that next == prev == &head (empty list). Appending is done by a single templated helper, addArgumentIndirectionArgumentOrOutput<T> (@ 0x237780), keyed by InstructionArgumentType: 0 → +0xA0, 1 → +0xB0, 2 → +0xC0. List order is insertion order. The nodes are bir::Argument subclasses ({vptr, next, prev, ArgumentKind, parent Inst*, id} at +0x00/+0x08/+0x10/+0x18/+0x20/+0x28); the operand model is documented in bir/value-model.md.


2. The +0xD0 Scheduling/Dependency Block

Instruction+0xD0 points at a heap aggregate the ctor builds with operator new(0xA98) followed by memset(…, 0, 0xA98). It is accessed everywhere as *((qword*)this + 26) + K (26 × 8 = 0xD0). The constant K is the literal offset within the block. It holds the dependency edge lists, the symbolic and resolved schedule slots, the sync info, and the debug provenance — the per-instruction scheduling state that is logically distinct from the IR-structural header.

Block offTypeMeaning / JSON keyAnchorConf
+0x008concurrent-set headernormal-dependency set; [+0] = element count (numDependencies())numDependencies @ 0x2d69c0; addDependency @ 0x2e6e90CERTAIN
+0x020intrusive list head"dependencies" — serialized ordered edge listtoJson walks blk+0x20CERTAIN
+0x270qword headdescendents / successors list headclearDescendentsCERTAIN
+0x4A8concurrent-set headerloop-carried dependency set; [+0] = element countaddDependency @ 0x2e6e90CERTAIN
+0x4C0intrusive list head"loop_carried_dependencies" — serialized ordered listtoJson walks blk+0x4C0CERTAIN
+0x948/+0x950vector<…> begin/end"unroll_dependencies"toJson +2376/+2384CERTAIN
+0x960bir::OpDebugInfo (opt)"debug" / ir_debug_info; flag byte @+0x960getDebugInfo @ 0x2d82c0: *(this+26)+2400; toJson guard *(blk+2400)CERTAIN
+0x9F0bir::SyncInfo (opt)"sync_info"; payload maps @+0x9F8/+0xA30; flag @+0x9F0getSyncInfo @ 0x2d7dd0: *(this+26)+2544; setSyncInfo @ 0x2e49a0CERTAIN
+0xA68/+0xA70vector<QuasiAffineExpr>"order" (symbolic multi-dim schedule index) begin/endhasOrder @ 0x2d8ea0 (blk+2664/+2672); toJsonCERTAIN
+0xA80QuasiAffineExpr* (cache)linearized loopnest-expr cache (lazy)getLoopnestExpr @ 0x2d76e0 (blk+2688)CERTAIN
+0xA88int64"scheduled_start"toJson [p+0xA88]; ctor zeroesCERTAIN
+0xA90int64"scheduled_end"toJson [p+0xA90]CERTAIN

Dependencies are stored twice

Each edge is held in two independent structures, and a reimplementer must maintain both.

The authoritative container is a tbb::concurrent_unordered_set<bir::EdgePtr> — one per edge class, with its header inline in the block at +0x08 (normal) and +0x4A8 (loop-carried). addDependency(EdgePtr, bool loop_carried) picks between them with a cmov on the flag, so the two classes never share a set. The header is the standard TBB layout: element count at [base+0x00] (bumped with a lock xadd, which is what numDependencies() returns), segment-table size at [base+0x08] used as the hash divisor, a 4.0f max-load-factor at [base+0x10], and a 63-segment bucket array occupying [base+0x40 .. base+0x240).

The serialization container is a separate intrusive list, headed at +0x20 and +0x4C0, which toJson walks to emit "dependencies" and "loop_carried_dependencies". It preserves edge order; the set does not.

The split matters because the two answer different questions: the set gives O(1) membership and a lock-free concurrent insert for the scheduler's parallel passes, while the list gives the deterministic ordering the JSON round-trip needs. Writing an edge to only one of them yields an instruction that either loses a dependency on reload or reports the wrong numDependencies().

Anchors: addDependency @ 0x2e6e90 (container select, lock xadd, comiss load-factor); toJson @ 0x2e31d0 / 0x2e3d7a (list walks, sentinel bit byte[head+8]&1).

NOTE — symbolic vs resolved schedule. "order" (+0xA68, a vector<QuasiAffineExpr>) is the symbolic multi-dimensional schedule key; "scheduled_start"/"scheduled_end" (+0xA88/+0xA90, scalar int64) are the resolved cycle slots. The pre-scheduling representation carries order; the post-scheduling representation fills scheduled_start/end. There is no getHeight/getDepth accessor on Instruction — schedule height/depth are computed over the edge lists and the order vector, not stored as named base fields.

What is not serialized

The block is mostly scheduler scratch. Only the dependency, loop-carried, and unroll lists, order, scheduled_start/_end, sync_info, and debug reach JSON; everything else is internal state the scheduler passes own.

The ctor seeds four self-relative list/map roots that toJson never reaches — +0x40, +0x290, +0x4E0, +0x730 — and writes 4.0f (0x40800000) at +0x268, +0x4B8, and +0x708. That float is not a weight or a priority: it is the max-load-factor field of an embedded TBB concurrent set, the same constant addDependency compares against with comiss. +0x4B8 is the load factor of the loop-carried set headed at +0x4A8; the copies at +0x268 and +0x708 mark two further concurrent sets in the block whose accessors are not traced.

INFERRED — what those two further sets hold is unknown. Given their position — bracketing the descendents head at +0x270 — they are most likely the successor-side mirrors of the two dependency sets, but no named accessor reads them.


3. The Constructor

bir::Instruction::Instruction(name, parent, IT) @ 0x2dafb0 is the single point that establishes the layout above. It is short and entirely a sequence of field stores plus the block allocation — no opcode-specific logic. Annotated:

// bir::Instruction::Instruction(string& name, BasicBlock* parent, InstructionType it)
//   @ 0x2dafb0   (a1=this, a2=&name, a3=parent, a4=it)
void Instruction(void* this, string* name, BasicBlock* parent, int it) {
    // --- NamedObject sub-object: name std::string with SSO ---
    *(OWORD*)(this + 0x08) = 0;             // NamedObjectBase holder slot
    *(char**)(this + 0x18) = this + 0x28;   // name.data -> SSO buffer (empty)
    sub_2D3970(this + 0x18, name->data, name->data + name->size);  // assign name

    *(BasicBlock**)(this + 0x50) = parent;  // parent BB
    *(int*)(this + 0x38)         = 0;       // origin = Internal
    *(int*)(this + 0x48)         = -1;      // unique_id sentinel
    *(qword*)(this + 0x40)       = 15;      // optin_passes default mask

    int uid = NamedObject::getUniqueId(this + 0x08);
    if (*(int*)(this + 0x48) != -1)         // self-check: must still be the sentinel
        throw runtime_error("Id has already been initialzed");   // (sic)
    *(int*)(this + 0x48) = uid;

    *(int*)(this + 0x58) = it;              // InstructionType tag  <-- THE dispatch key

    *(OWORD*)(this + 0x60) = 0;             // loopnest vector  (begin/end)
    *(OWORD*)(this + 0x70) = 0;             // loopnest.cap + predicates.begin
    *(OWORD*)(this + 0x80) = 0;             // predicates end/cap
    *(qword*)(this + 0x90) = 0;             // engine + engine_id = 0
    *(int*)(this + 0x98)   = 0;             // scratch

    *this = &_ZTVN3bir11InstructionE[2];    // vptr (= symbol + 0x10)

    // three operand sentinels: next = prev = &head  (movddup of the head's own addr)
    *(m128*)(this + 0xA0) = dup(this + 0xA0);   // ins
    *(m128*)(this + 0xB0) = dup(this + 0xB0);   // indirection_ins
    *(m128*)(this + 0xC0) = dup(this + 0xC0);   // outs

    // --- the heap scheduling/dependency block ---
    void* blk = operator new(0xA98);
    memset(blk, 0, 0xA98);
    *(byte*)(blk + 0x10)  = 8;
    *(word*)(blk + 0x1A)  = 16512;
    *(qword*)(blk + 0x30) = 1;
    *(qword*)(blk + 0x40) = blk + 0x48;     // dependencies list root (self-relative)
    /* … seed the descendents / loop-carried / map roots at +0x290/+0x4E0/+0x730,
       write 4.0f (0x40800000) at the per-edge-weight slots,
       zero the order/sched/sync/debug regions … */
    *(byte*)(blk + 0x960) = 0;              // debug optional = empty
    *(byte*)(blk + 0x9F0) = 0;              // sync_info optional = empty
    *(OWORD*)(blk + 0xA68) = 0;             // order vector empty
    // scheduled_start/end already zero from memset

    *(void**)(this + 0xD0) = blk;           // hang the block off +0xD0
    *(qword*)(this + 0xD8) = 0;             // tail scratch (op payload starts here)
    *(qword*)(this + 0xE0) = 0;
    *(qword*)(this + 0xE8) = 0;
}

The throw on a non--1 id is not dead code: getUniqueId writes +0x48 through the CRTP sub-object, and the ctor verifies the write landed on a fresh object. It is the only control flow in the ctor besides the block-init loops.


4. The JSON Header — 19 Keys

bir::Instruction::toJson(json&) @ 0x2e2ea0 emits the common header shared by all opcodes; each Inst<X>::toJson chains this base first, then appends its own keys. The reverse path is createFromJsonHelper @ 0x2e01b0 (opcode string → per-op createFromJsonreadFieldsFromJson). The keys below are emitted in this exact order, each pinned to its source offset and emit guard in the toJson body.

#JSON keySourceTypeEmitted whenConf
1"origin"INLINE +0x38NamedObjectOriginalwaysCERTAIN
2"opcode"INLINE +0x58 (IT)InstructionType2stringalwaysCERTAIN
3"engine"INLINE +0x90EngineTypealwaysCERTAIN
4"engine_id"INLINE +0x94uint32alwaysCERTAIN
5"scheduled_start"BLK +0xA88int64alwaysCERTAIN
6"scheduled_end"BLK +0xA90int64alwaysCERTAIN
7"optin_passes"INLINE +0x40array<string> (bit→passname)alwaysCERTAIN
8"dependencies"BLK +0x20 listarray<{id,kind}> (EdgeKind & 7)numDependencies != 0CERTAIN
9"loop_carried_dependencies"BLK +0x4C0 list (guard +0x4A8)array<{id,kind}>list non-emptyCERTAIN
10"unroll_dependencies"BLK +0x948/+0x950array<…>begin != endCERTAIN
11"sync_info"BLK +0x9F0object (SyncInfo::toJson)flag byte @+0x9F0 setCERTAIN
12"ins"INLINE +0xA0 listarray<Argument>always (empty OK)CERTAIN
13"indirection_ins"INLINE +0xB0 listarray<Argument>list non-emptyCERTAIN
14"outs"INLINE +0xC0 listarray<Argument>alwaysCERTAIN
15"debug"BLK +0x960object (OpDebugInfo::to_json)flag byte @+0x960 setCERTAIN
16"loopnest"INLINE +0x60 vectorarray<string> (LoopAxis names)begin != endCERTAIN
17"predicates"INLINE +0x78 vectorarray<AffinePredicate> (stride 40)begin != endCERTAIN
18"order"BLK +0xA68/+0xA70 vectorarray<QuasiAffineExpr>begin != endCERTAIN
19"dma_trigger_debug_update_semaphore_id"per-op (IT == 67)IT == InstDMATriggerHIGH

The first 18 keys are the genuine base header. Key 19 is emitted by the base toJson body via an IT == 67 guard (if (*(int*)(*(this)+0x58) == 67)), reading a DMABlock operand through InstDMATrigger::getDmaBlock rather than a base field — it lives in the base serializer only because the DMATrigger debug hook is wired there. Op-specific keys proper (e.g. the matmul family's 13 keys at +0xF0..+0x328) are appended by the leaf toJson after this base returns.

NOTE — the four short keys "opcode", "engine", "ins", "outs", "order" do not appear as standalone entries in the strings table because nlohmann constructs them as inline operator[] key literals; they are confirmed by the lea-string operands in the toJson body at the line offsets above (e.g. the "dependencies" key is corroborated by the adjacent assert "'dependencies' must be a JSON array").


5. Dependency Edges: the PointerIntPair Model

Each dependency edge is an llvm::PointerIntPair<bir::Instruction*, 3>: a single machine word whose low 3 bits hold the EdgeKind and whose high bits (mask ~7) hold the target Instruction*. This is proven byte-exact by the asserts in addDependency(Instruction*, EdgeKind, bool) @ 0x2e7640:

// bir::Instruction::addDependency(Instruction* former, EdgeKind kind, bool loop_carried)  @ 0x2e7640
int addDependency(Instruction* this, Instruction* former, char kind_and_lc) {
    assert(this != former && "this != former");                 // Instruction.cpp:0x150
    assert((former & 7) == 0  && "Pointer is not sufficiently aligned");   // PointerIntPair.h:0xCA, IntBits=3
    assert((kind  & 0xF8) == 0 && "Integer too large for field"); // PointerIntPair.h:0xD1, IntMask=~7
    // append { former | kind } to the dependencies list (loop_carried selects the head)
    …
}

The edge node layout (in either list) is { next@+0x00, byte&1 sentinel-flag @+0x08, EdgePtr @+0x10 }. toJson decodes each node as EdgeKind2string(*(node+0x10) & 7) for the kind and *(node+0x10) & ~7 for the target.

GOTCHA — kinds are tags, lists are heads, and they are orthogonal. There are two edge list heads in the block — normal dependencies (head +0x20, count +0x08) and loop_carried_dependencies (head +0x4C0, guard +0x4A8) — chosen by addDependency's loop_carried argument. The four edge kindsEdgeKind = {Invalid 0, Ordered 1, Anti 2, Output 3, Flow 4} — are not separate heads; they are the 3-bit tag on each node within whichever list. FLOW vs ANTI vs OUTPUT is a per-edge property; normal vs loop-carried is a per-list property. (The EdgeKind value→name mapping is recovered from the EdgeKind2string switch, not pinned to a named enum — IDA carries no EdgeKind enum.)

The +0x270 head holds the reverse descendents/successors list, maintained alongside the predecessor edges and cleared by clearDescendents.


6. The Subclass Graph

There is exactly one polymorphic root, bir::Instruction (_ZTSN3bir11InstructionE). The tree is two levels deep for most opcodes, three for five families, and four for two chains. Six InstructionType values are abstract intermediate bases — never instantiated directly, their concrete children carry the real opcode:

ITAbstract baseFamily / accept-setChildren
0InstGeneric{≤3}InstGenericCopy(1), InstGenericRelu(2), InstEventSemaphore(13) → InstAbstractCopy(3)
7InstMatmultBase{7,8,9,95}InstMatmult(8), InstMatmultSparse(9), InstMatmultMx(95)
18InstDMA{18,19,22,32,41-46,67}InstLoad(19) → InstSave(22), InstDMACopy(32), InstDMATrigger(67), InstTensorLoad(75), InstTensorSave(76)
47InstCollective{47,48,49,50}InstCollectiveCompute(48), InstCollectiveSend(49), InstCollectiveRecv(50)
68InstDMADescriptor{68-72}…Copy(69), …CCE(70), …Transpose(71), …Replicate(72)
77InstTerminator{77,78,79,81,82,83}CompareAndBranch(78), UnconditionalBranch(79), Return(81), Exit(82), Break(83)

These six are exactly the ITs that route to the base visitInstruction in the simulator and have family-level bodies in the verifier — three independent confirmations that they are abstract. The two deep chains, both confirmed by ctor base-calls: Instruction → InstGeneric → InstEventSemaphore → InstAbstractCopy and Instruction → InstDMA → InstLoad → InstSave (where InstSave::readFieldsFromJson tail-jumps to InstLoad's — "Save = Load on the wire"). All other ~90 opcodes derive directly from bir::Instruction. The full leaf→opcode crosswalk is the InstructionType table; see bir/instruction-type.md.

QUIRK — InstDMADescriptor is NOT under InstDMA. Despite the name, the descriptor family (IT 68–72) derives directly from Instruction, not from InstDMA: its ctor calls Instruction::Instruction directly and the subtree has no dge_type/queue field. Likewise InstQuantizeMx(96) is a sibling of the matmul family, not a InstMatmultBase child. And InstBranchHint(80) is a control op that is excluded from terminator-class equality — its base edge is argued from that exclusion rather than read from a ctor base-call [INFERRED].

The 113-vs-401 reconciliation

A naive count of typeinfo-name hits mentioning a bir::Inst type returns ~401. That is not the class count. The actual polymorphic population, counted as distinct _ZTSN3bir…Inst* typeinfo strings in the RTTI table, is 113:

AxisCountSource
Distinct _ZTS bir-Inst typeinfo strings113rtti.json (rg '^_ZTSN3bir.*Inst' | sort -u)
of which: root + CRTP base2InstructionE, NamedObject<Instruction,BasicBlock>E
of which: abstract intermediate bases6Generic, MatmultBase, DMA, Collective, DMADescriptor, Terminator
⇒ concrete leaf classes (own typeinfo)105matches the 105 validEngines maps (§7)

The inflation from 113 to 401 is (a) the per-class RTTI triplet_ZTV vtable / _ZTI typeinfo / _ZTS type-string (~3×) — and (b) template instantiations whose mangled names merely mention an Inst type (ilist_iterator<…Instruction…>, DenseSet<Instruction const*>, the ~60 bir::Hwm::getLatency(InstX&) overloads, the visitInst<X> method symbols). None of those are subclasses. The hard floor is independent: getValidEngines is overridden by ~105 concrete leaves, and there are 113 typeinfo strings — both an order of magnitude below 401.

NOTE — the 113 here is the loose-typeinfo lens. The 113-string count (_ZTSN3bir.*Inst, including the NamedObject<Instruction,…> template) and the strict-RTTI-triplet count (112, _ZTSN3bir[0-9]+Inst…E only) are two of six interlocking lenses on one ~112-class population. The full 121 / 113 / 112 / 110 / 104–105 decomposition — with the nine non-IR symbol-token contaminants enumerated — lives in the canonical reconciliation table at cross-language wire-keys §2. Use 112 for the polymorphic-class population, 110 for opcodes, 104–105 for concrete leaves; this page's 113 ⇒ 105 is the loose-grep route to the same floor.


7. The Shared Vtable

The vtable is _ZTVN3bir11InstructionE @ 0x8fced8; the object's vptr is the symbol +0x10 (= 0x8fcee8, past the Itanium {offset-to-top=0, &typeinfo} header). Every leaf installs a same-shaped vtable with the same slot meanings. The single hard, reloc-proven slot is getValidEngines at vt+0x60 (slot 12): on InstMatmult's vtable the slot relocates to _ZNK3bir11InstMatmult15getValidEnginesEv, and each of ~105 concrete leaves overrides it with an 8-byte stub returning its own ::validEngines table. The matmul family alone shows the override on InstMatmult, InstMatmultSparse, InstMatmultMx, and the abstract InstMatmultBase.

The central design point is that the vtable is small: most per-opcode behavior is reached not through the vptr but through the InstructionType switch at +0x58.

DispatchMechanismAnchorConf
getValidEnginesvirtual, vt+0x60reloc on InstMatmult vtCERTAIN
toJsonvirtual (leaf chains base)base @ 0x2e2ea0CERTAIN
getDebugInfoSourcevirtual, slot 2HIGH
destructor + Itanium pairvirtualHIGH
visitor visit/acceptopcode switch on +0x58 (110 arms)sim + verifier dispatchCERTAIN
sameInstbase 110-arm switch + 16 leaf delegatesbase @ 0x2db7b0CERTAIN
createFromJsonstatic factory (not virtual)createFromJsonHelper @ 0x2e01b0CERTAIN
verifyexternal (libwalrus visitor); one libBIR bodyInstGPSIMDSB2SB::verify @ 0x294e00CERTAIN

QUIRK — no accept() virtual. BIR does not use C++ double-dispatch. Both the simulator and the birverifier dispatch with an explicit 110-arm switch on Instruction+0x58. There is no per-leaf accept() vtable slot; the IRVisitor/InstVisitor switch is the dispatch. sameInst is a hybrid — a base switch that tail-calls a real per-leaf override for only 16 opcodes (Activation, MatmultBase, the TensorScalar trio, DMACopy, StreamShuffle, CollectiveCompute, CustomOp, BIRKernel, Rand, Iota, AffineSelect, RangeSelect, GetCurProcessingRankID, CoreBarrier). And there is essentially no Instruction::verify in libBIR at all: verification lives in libwalrus's birverifier (bir/value-model.md and the verifier page), keyed by the same opcode int.


Cross-References

  • bir/instruction-type.md — the 110-value InstructionType enum and the full leaf→opcode crosswalk (the +0x58 tag's value space).
  • bir/value-model.mdbir::Argument operand subclasses, ArgumentKind, and the ins/indirection_ins/outs list contents.
  • bir/brewer-generator.md — the Python brewer layer that emits BIR JSON consumed by createFromJson.
  • nki/bircodegenloop.md — the Penguin→BIR driver that constructs these instructions.