Codegen AccessPattern Primitive — codegenAccess / BirAccessPattern
Every symbol, address, offset, and string on this page applies to
neuronx_cc2.24.5133.0+58f8de22 (cp310), moduleneuronxcc/starfish/lib/libwalrus.so(ELF64, 64,973,024 bytes;VA == file-offsetfor.text@0x62d660and.rodata@0x1c72000). The same backend is statically linked intonki_klr_sim. Other wheels differ; treat every address as version-pinned. Evidence is the IDA decompiled/disasm sidecars underneuronx-cc/ida/…libwalrus.so/. Note the two-VA-frame artifact: each function has a 6-byte.-prefixedjmpthunk at low.text(0x5f…/0x61…) and the real body at0xf1…; this page cites the real bodies.
Abstract
This page documents the shared klr→BIR access-pattern construction spine of the beta2 / klr codegen path: the one descent that every per-op KLR→BIR leaf (the I-ops that lower nisa/nki tensor instructions) walks to turn a klr::TensorRef operand into the (stride, size)-pair descriptor a BIR Instruction carries. It is the beta2 counterpart of the beta3 Penguin builder family documented in BirCodeGenLoop Access-Pattern Builders; the two paths converge on the same abstract (step, num) AP model but split sharply on where the set-algebra runs — beta2's primitive is a pure relay, beta3's is not.
The spine is four C++ encoders plus a symbolic side-channel. codegenTensorRef → codegenTensorRefImpl dispatches on the klr::TensorRef kind: SBUF/PSUM accesses go to codegenAccess, DRAM/HBM tensors to codegenHbm. codegenAccess in turn switches the inner klr::Access kind — the general strided path codegenBirAccessPattern (slices, transposes, broadcasts) or the contiguous fast path codegenAccessSimple (a bare name, no explicit AP list). Both general encoders bottom out in codegenAP, the lowest-level materialiser that copies a std::list<klr::APPair> verbatim into a SmallVector<APPair,4>. All four encoders write the same transient InstArg operand descriptor that InstBuilder::addArgument<PhysicalAccessPattern> later mints into the real bir::PhysicalAccessPattern (kind 1).
The one decision that shapes the whole layer is the static-vs-symbolic split. The four encoders always build a physical (kind-1) operand. A symbolic (kind-2) AP is produced only on the dynamic path, gated by isAccessDynamic at the call site (not inside any encoder) and built by assembleDynamicInfo, which mints a pelican::Expr address-algebra expression — IndirectArgExpr (ExprKind 3) for a vector/gather offset, BirIntRuntimeValue (ExprKind 7) for a scalar/register offset. That branch is the QUIRK material on this page (§6).
| Module | libwalrus.so cp310 (class neuronxcc::backend::KlirToBirCodegen) |
| Dispatch entry | codegenTensorRef 0xf18b30 → codegenTensorRefImpl 0xf18960 |
klr::Access switch | codegenAccess 0xf18820 — kind 4 → codegenBirAccessPattern, kind 1 → codegenAccessSimple |
| General strided | codegenBirAccessPattern 0xf18430 (664 B) — ML-by-name + list copy → codegenAP |
| Contiguous fast | codegenAccessSimple 0xf186d0 (331 B) — 1-pair identity AP, shape {1,1,1} |
| List materialiser | codegenAP 0xf13f50 (214 B) — klr::APPair{i32,u32} → bir::APPair{i64,i64}, pure relay |
| DRAM path | codegenHbm 0xf15060 (629 B) — 2-dim AP, byte-size ÷ dtype-bytes |
| Dynamic predicate | isAccessDynamic(BirAccessPattern) 0xf14030 — [ap+0x40] | [ap+0x58] |
| Symbolic builder | assembleDynamicInfo 0xf20230 (2683 B) — pelican::Expr kind 3 / kind 7 |
| Transient output | InstArg — {MemLoc*, SmallVector<APPair,4>, partition:i32, dtype:i32, valid:u8} |
| Wire consumers | addArgument<PhysicalAccessPattern> @plt 0x5fcd80 · addOutput @plt 0x5fc5e0 |
This page builds the transient codegen output; the value-object that consumes it — bir::PhysicalAccessPattern and the Argument hierarchy — is BIR Value Model (note the offset distinction in §5), the on-wire ADDR4 / TENSOR1-4D silicon descriptors are Part 2, and the kind-2→kind-3 lowering is lower_ap (cross-ref §7). The dynamic-AP companion DynamicAPINFO is detailed in Dynamic Access Patterns.
1. The dispatch spine — the path every I-op walks
A per-op codegen never builds an access pattern inline. It binds each tensor operand through exactly one entry, codegenTensorRef, and lets the spine resolve the operand class. The descent is fixed:
codegenTensorRef(ref) §1.1 shared_ptr<klr::TensorRef>
→ codegenTensorRefImpl(ref, allow_dynamic) §1.2 isAccessDynamic gate
├ TensorRef.kind == 1 → codegenAccess(ref+0x08) §1.3 SBUF / PSUM
│ ├ Access.kind == 4 → codegenBirAccessPattern §3 general strided
│ └ Access.kind == 1 → codegenAccessSimple §2 contiguous fast
└ TensorRef.kind == 4 → codegenHbm(...) §4 DRAM / HBM
Every leaf produces one InstArg (§5); the call site then hands it to addArgument/addOutput<PhysicalAccessPattern> to mint the real BIR operand. This is the single funnel: there is no per-op AP code, so a reimplementation that gets this descent right gets every op's operand binding right for free.
1.1 codegenTensorRef 0xf18b30 — the entry
Shallow-copies the 16-byte shared_ptr<klr::TensorRef> into a stack temp, bumps the strong-count, and tail-calls codegenTensorRefImpl(this, BB, &temp, /*allow_dynamic=*/0). The hard-coded allow_dynamic = false is the default contract: most ops forbid dynamic access. The dynamic-capable ops (e.g. nisa.dma_copy with a runtime offset) bypass this wrapper and call codegenTensorRefImpl directly with allow_dynamic = true.
1.2 codegenTensorRefImpl 0xf18960 — the gate + kind switch
// Address 0xf18960 (real body; thunk 0x61bb20)
InstArg codegenTensorRefImpl(shared_ptr<klr::TensorRef> ref, bool allow_dynamic) {
// --- the static/symbolic GATE (decompiled v10) ---
bool v10 = isAccessDynamic(ref) & (allow_dynamic ^ 1); // dynamic AND not-allowed
if (v10)
throw std::runtime_error("Dynamic Access is not allowed in the instruction.");
switch (*(i32*)*ref) { // klr::TensorRef kind @ +0x00
case 1: return codegenAccess(*(ref + 0x08)); // inner klr::Access @ +0x08
case 4: return codegenHbm(...); // DRAM / HBM tensor
default:
__assert_fail("false && \"Unsupported tensorRef type encountered!\"",
".../klir_to_bir_codegen.cpp", 0x135, ...);
}
}
The gate is isAccessDynamic(ref) & (allow_dynamic ^ 1) — i.e. dynamic AND not-allowed throws. The assert string and the klir_to_bir_codegen.cpp:0x135 source location are present verbatim in the binary; the kind-1 branch loads the inner klr::Access shared_ptr at TensorRef+0x08 and forwards it to codegenAccess. [evidence: decompiled body 0xf18960 — v10 = isAccessDynamic(...) & (a4 ^ 1); v12 == 1 → codegenAccess, v12 != 4 → __assert_fail.]
1.3 codegenAccess 0xf18820 — the klr::Access kind switch
// Address 0xf18820 (real body; thunk 0x60bb60)
InstArg codegenAccess(shared_ptr<klr::Access> access) {
switch (*(i32*)*access) { // klr::Access kind @ +0x00
case 4: return codegenBirAccessPattern(this, BB, access);
// FULL strided AP (slice / transpose / broadcast)
case 1: return codegenAccessSimple(this, BB, /*klr::TensorName=*/ access + 0x08);
// contiguous fast path; forwards inner TensorName
default: throw std::runtime_error("Unsupported access type encountered " + to_string(kind));
}
}
The kind-1 branch does not forward access itself — it loads the inner klr::TensorName shared_ptr at klr::Access+0x08 (the decompiler's _mm_loadu_si128((__m128i*)(v7 + 2)) = ptr+ctrl at +0x10 byte offset, i.e. +0x08 qword) and passes that to codegenAccessSimple. [evidence: decompiled 0xf18820 — v8 = *(i32*)*a3, v8 == 4 → codegenBirAccessPattern, v8 == 1 → loads a3[1]/(v7+2) → codegenAccessSimple, else "Unsupported access type encountered " + to_string.]
2. codegenAccessSimple 0xf186d0 — the contiguous fast path
Used when klr::Access.kind == 1: a bare klr::TensorName with no explicit AP list. The encoder synthesises a single-dim identity AP spanning the whole tensor along the free axis and never reads an AP list.
// Address 0xf186d0 (real body; thunk 0x61e870)
InstArg codegenAccessSimple(shared_ptr<klr::TensorName> klr) {
MemoryLocation* ML = Function::getMemoryLocationByName(klr.name); // BB.Func @ +0x40
if (!ML) ML = codegenTensorName(klr); // fallback: mint a fresh MemLoc
u64 num = **(u32**)(ML + 0x100); // ML+0x100 → dims ptr → [0] = elem count
u32 step = *(u32*)(klrTensorName + 0x48); // TensorName stride
InstArg a;
a.Pattern = SmallVector<APPair,4>{ {step, num} }; // ONE dim; header 0x400000000 (size 0, cap 4)
a.shape = SmallVector<u64>{ 1, 1, 1 }; // {1,1,1}; header 0x400000003 (size 3, cap 4)
a.MemLoc = ML; // InstArg+0x00
a.partition = 0; // InstArg+0x58 = 0 (no partition offset)
a.dtype = MemoryLocation::getDtype(ML); // InstArg+0x5C native dtype (no per-access cast)
a.valid = 1; // InstArg+0x60
return a;
}
This is the access an op produces when the front-end handed it a name with no sliced AP: one canonical free dim {step = TensorName.stride, num = ML.elem_count}, the shape defaulted to 1×1×1, the partition field zero, and the dtype taken straight from the MemoryLocation — no per-access cast on this path. [evidence: decompiled 0xf186d0 — v5 = *MemoryLocationByName[32] (ML+0x100 deref, [0] = num); v6 = *(u32*)(*a3 + 72) = TensorName+0x48 (step); v10[0]=step, v10[1]=num, v10[2..4]=1,1,1; v9 = 0x400000003 (shape header); a1[5].i32[2] = 0 (partition); a1[5].i32[3] = getDtype(ML); a1[6].i8[0] = 1 (valid).]
QUIRK — Simple reads the element count directly; Hbm divides.
codegenAccessSimpleliftsnumstraight out of theMemoryLocationdims pointer atML+0x100.codegenHbm(§4) cannot — a DRAMMemoryLocationstores its size in bytes, so it dividesML+0x108by the per-dtype byte size to recover the element extent. Same abstract AP, two different extent derivations.
3. codegenBirAccessPattern 0xf18430 — the general strided path
Used when klr::Access.kind == 4: an explicit klr::BirAccessPattern carrying a list of {step, num} pairs. This is where slices, transposes, and broadcasts live.
3.1 klr::BirAccessPattern field map
| off | type | meaning |
|---|---|---|
+0x00 | i32 | kind (== 4 as seen by codegenAccess) |
+0x10 | i32 | partition / base-index field → InstArg.partition |
+0x18 | ListHead | std::list<shared_ptr<klr::APPair>> sentinel; nodes walked via [node] |
+0x38 | name | tensor name (resolved via getMemoryLocationByName) |
+0x40 | u8 | scalar-dynamic-offset present (register / unique-indices) → ExprKind 7 |
+0x58 | u8 | vector-dynamic-offset present (gather index) → ExprKind 3 |
+0x64 | i32 | dtype-override index (klr::Dtype, fed to codegenDtype) |
+0x68 | u8 | dtype-override-PRESENT flag |
The byte offsets are read directly from the disasm of the four encoders and the validator. The klr struct header itself was never separately mapped, so the field roles in the right-hand column are inferred from how each offset is used, not from a declaration.
klr::APPair (the 8-byte list payload): +0x00 i32 step (signed), +0x04 u32 num. The list node is 24 B — _List_node_base{next,prev} at +0x00/+0x08, then shared_ptr<APPair>{ptr @+0x10, ctrl @+0x18}; codegenAP reads node+0x10 (the .get() pointer).
3.2 The body
// Address 0xf18430 (real body; thunk 0x60be00)
InstArg codegenBirAccessPattern(shared_ptr<klr::BirAccessPattern> klr) {
MemoryLocation* ML = Function::getMemoryLocationByName(klr.name); // klr+0x38 = name
if (!ML) ML = codegenTensorName(klr);
// --- COPY the klr APPair list into a private working list (two passes) ---
// pass 1: dup klr.list@+0x18 head segment into stack list v18 (tc_new(32) per node,
// APPair shared_ptr strong-count bumped); pass 2: dup v18 into list `i`.
std::list<shared_ptr<klr::APPair>> work;
for (node in klr.list @ +0x18) {
n = tc_new(32); n.sp = node.sp(+0x10/+0x18); refbump; work.hook(n);
}
SmallVector<APPair,4> ap; codegenAP(&ap, this, &work); // §5; header 0x400000000
work.clear();
InstArg a;
a.MemLoc = ML; // InstArg+0x00
a.Pattern = ap; // SmallVector copy via sub_F12B40
a.partition = *(i32*)(klr + 0x10); // InstArg+0x58 partition / base idx
a.dtype = klr[+0x68] // InstArg+0x5C
? codegenDtype(klr[+0x64]) // explicit per-access cast
: MemoryLocation::getDtype(ML); // else native MemLoc dtype
a.valid = 1; // InstArg+0x60
return a;
}
[evidence: decompiled 0xf18430 — getMemoryLocationByName() ?: codegenTensorName(); two tc_new(32) hook loops (v18 head segment, then i); codegenAP(&v22, a2, &i); a1[1] = 0x400000000; a1[5].i32[2] = *((i32*)*a3 + 4) = klr+0x10 (partition); dtype branch if *((u8*)*a3 + 104) = klr+0x68 → codegenDtype(*((i32*)*a3 + 25)) = klr+0x64, else getDtype(ML); a1[6].i8[0] = 1.]
3.3 What the general path does not do
- No start address.
codegenBirAccessPatterncarries no byte offset of its own. The base is theMemoryLocation(resolved by name); the per-dim start is the partition field (klr+0x10) plus the AP geometry. The eventual byte offset (the wireADDR4start) is computed downstream byInstBuilder/lower_ap, not here — the body contains no offset store at all. - No stride scaling. Each
{step, num}passes throughcodegenAPverbatim; the dtype→bytes scaling (qword_1DE98C0[dtype]=[1,1,2,1,1,1,1,1,4,4,2,2,2,2,4,4,4,4,8,8]) happens at the wire encoder, not in codegen. - No partition-axis search. The canonical
[W,Z,Y,X]axis order meansPattern[0]is the partition/W axis by construction. The codegen relays the front-end list order plus the scalar partition/base index; there is no "which dim is the 128-partition axis" search. The KLR front-end already ordered the list soPattern[0] = partition. - No max-of-inputs dtype promotion. Dtype is per-operand: either the explicit per-access cast (
klr+0x68set) or the nativeMemoryLocationdtype. Promotion, if any, is an upstream KLR cast.
4. codegenAP 0xf13f50 — list → SmallVector<APPair,4>, the pure relay
The lowest-level materialiser. No transformation, no broadcast logic, no partition logic — it copies a std::list<shared_ptr<klr::APPair>> into a SmallVector<APPair,4>, widening each {i32, u32} pair to {i64, i64}.
// Address 0xf13f50 (real body; thunk 0x5f51f0)
SmallVector<APPair,4> codegenAP(std::list<shared_ptr<klr::APPair>> list) {
out.data = &out.inline_buf;
out.header = 0x400000000; // size = 0, cap = 4 (qword_1DBCE40)
for (node in list) {
klr::APPair* p = node[+0x10]; // shared_ptr .get()
i64 step = (i64)(i32)p[0x00]; // SIGN-extend the 32-bit step (movsxd)
i64 num = (u64)(u32)p[0x04]; // zero-extend the 32-bit num
if (out.size + 1 > out.cap)
llvm::SmallVectorBase<u32>::grow_pod(); // spill past 4 dims
out[out.size] = APPair{step, num}; // 16-byte write {step @+0, num @+8}
// re-asserts N <= capacity() ("SmallVector.h":0x5A)
out.size++;
}
return out;
}
The sign-extension of step is decisive and confirmed at the byte level: 0xf13f8f: movsxd r13, dword ptr [rdx] (step, signed → i64) versus 0xf13f8c: mov ebp, [rdx+4] (num, 32→64 zero-extend). Negative strides — reverse / descending access — therefore survive into the BIR AP; num (an extent) cannot be negative and is zero-extended. [evidence: disasm 0xf13f50 — header from qword_1DBCE40 = 0x400000000; mov rdx,[rbx+10h] (node+0x10); movsxd r13,[rdx]; mov ebp,[rdx+4]; stores [rax]=r13, [rax+8]=rbp; grow_pod at 0xf13ff3; __assert_fail("N <= capacity()", ".../SmallVector.h", 0x5A, ...).]
QUIRK — a broadcast dim is just
step == 0, and codegen never inspects it.codegenAPrelays a stride-0 entry verbatim. Partition-broadcast (Pattern[0]/W) versus free-broadcast (Y/X) is not distinguished here — both are zero-step entries, told apart only by which slot carries the zero. Codegen neither creates nor validates the broadcast; the KLR front-end mints it. A reimplementer must not add a "broadcast detector" to this layer — there isn't one.
5. The transient InstArg operand descriptor
All four encoders write the same ~0x68-byte stack struct — the "PhysicalAccessPattern temp." It is not a bir::AccessPattern; it is the recipe that InstBuilder::addArgument/addOutput<PhysicalAccessPattern> consume to mint the real bir::PhysicalAccessPattern.
| off | field | source |
|---|---|---|
+0x00 | MemoryLocation* | getMemoryLocationByName (or codegenTensorName fallback) |
+0x08 | SmallVector<APPair,4>.data | self-ptr = &InstArg+0x18 inline buffer |
+0x10 | SmallVector header | size:u32 @+0x10, cap:u32 @+0x14; init 0 / 4 |
+0x18 | APPair[4] inline buf | 4 × 16 B {step:i64, num:i64} (+ access_shape SmallVector on the Simple/Hbm variants) |
+0x58 | i32 partition | klr+0x10; 0 for Simple / Hbm |
+0x5C | i32 dtype | bir::Dtype — per-access cast or native MemLoc dtype |
+0x60 | u8 valid | set 0 at entry, 1 on success |
[evidence: r13-relative writes in codegenBirAccessPattern 0xf185c3..0xf18613 — a1[5].i32[2] = +0x58, a1[5].i32[3] = +0x5C, a1[6].i8[0] = +0x60.]
GOTCHA — the
InstArgSmallVector at+0x18is not thebir::AccessPatterndata at+0x50. This page's transientInstArgis a stack struct local to codegen; its Pattern SmallVector lives atInstArg+0x18. The minted value-object,bir::PhysicalAccessPattern, is a different thing entirely: it is multiple-inheritance —bir::Argumentat offset 0 plus alogging::SrcHandlevptr at+0x48, so the AP's own data (the twoSmallVector<APPair,4>s) resumes at+0x50(BIR Value Model §3). Do not conflate the two+0x18/+0x50layouts: theInstArgis consumed and discarded; the+0x50layout is whataddArgumentconstructs from it. This page is consistent with 7.4 precisely because it never claims theInstArgis the AP object.
How InstArg becomes a BIR AccessPattern. addArgument<PhysicalAccessPattern>(InstArg) allocates a PhysicalAccessPattern (kind 1) and copies the recipe: setLocation(InstArg.MemLoc), setPattern(InstArg.Pattern) (@libBIR 0x211d40, APPair stride 16), setType(InstArg.dtype), setOffset(...) (computed downstream), partition from InstArg+0x58. If the op allowed dynamic access and the access was dynamic, the call site first runs assembleDynamicInfo (§6) to attach a DynamicAPINFO — that is what promotes the operand to the symbolic (kind-2) family.
6. The static-vs-symbolic split — where kind-2 is minted
The four encoders in §2–§4 always build a physical (kind-1) operand. A symbolic (kind-2) AP is produced only on the dynamic path, and the decision is not inside any encoder — it is the isAccessDynamic predicate evaluated at the call site (§1.2), where the op decides allow_dynamic.
6.1 isAccessDynamic — the predicate
// Address 0xf14030 — isAccessDynamic(shared_ptr<klr::BirAccessPattern>), 11 bytes
u8 isAccessDynamic(shared_ptr<klr::BirAccessPattern> ap) {
return *(u8*)(*ap + 0x40) // scalar-dynamic-offset present
| *(u8*)(*ap + 0x58); // vector-dynamic-offset present
}
isAccessDynamic(Access) 0xf16540 returns false unless Access.kind == 4, then extracts the inner klr::BirAccessPattern at Access+0x10 and delegates to the above. So: an access is dynamic ⇔ it carries a scalar OR vector runtime offset (an indexed / gather address). Static accesses → physical (kind-1). Dynamic accesses → symbolic (kind-2), evaluated to a DynamicAPINFO. [evidence: decompiled 0xf14030 — result = *(u8*)(*a1 + 64), LOBYTE(result) = *(u8*)(*a1 + 88) | result; 0xf16540 — if (*(i32*)*a1 != 4) return 0, else loads inner AP at (v2+2).]
QUIRK — the static/symbolic decision lives at the call site, not in the encoder. A reimplementer reading
codegenBirAccessPatternwill find no kind-2 branch in it at all. The four encoders are unconditionally physical. The static-vs-symbolic fork is theisAccessDynamic(...) & (allow_dynamic ^ 1)gate incodegenTensorRefImpl(which only ever throws on the forbidden case) plus the separateassembleDynamicInfocall the dynamic-capable op makes aroundaddArgument. This mirrors the symbolic-AP→register-ALU split elsewhere in the backend: the symbolic form is built on a side-channel, never folded into the physical builder.
6.2 The scalar-XOR-vector invariant
// Address 0xf140a0 — _validateOnlyOneOfScalarAndVectorDynamicOffsetIsProvided
void _validate...(shared_ptr<klr::BirAccessPattern> ap) {
if (*(u8*)(*ap + 0x58) == *(u8*)(*ap + 0x40)) // scalar == vector → fail
assert_fail(...);
}
On the dynamic path, exactly one of scalar-offset (+0x40) / vector-offset (+0x58) is present — never both, never neither. [evidence: decompiled 0xf140a0 — if (*(u8*)(*a1 + 88) == *(u8*)(*a1 + 64)) sub_6D6825().]
6.3 assembleDynamicInfo 0xf20230 — the symbolic-AP builder
Builds a bir::DynamicAPINFO carrying the pelican::Expr symbolic address algebra. Two branches, selected by which offset flag is set:
(A) Vector offset (klr+0x58 set — a gather index tensor):
pelican::Expr::Expr(/*ExprKind=*/3, ctx)→pelican::IndirectArgExpr(its vtable_ZTVN7pelican15IndirectArgExprEis loaded right after the ctor).- Constraint:
indirect_dimmust be 0 — string "When using vector offset, the only supported indirectDim is 0" is present in the binary. (A gather indexes the W/partition axis.) - The index AP itself is recursed via
codegenAccess(throughextractDynamicApToStandAlone).
(B) Scalar offset (klr+0x40 set — a register-backed runtime integer):
Register* r = Function::getRegisterByName(name).pelican::Expr::Expr(/*ExprKind=*/7, ctx)→bir::BirIntRuntimeValue(vtable_ZTVN3bir18BirIntRuntimeValueEloaded after the ctor); the register backs the offset.
Both branches then build per-axis bir::QuasiAffineExpr(RefPtr<pelican::Expr>), accumulate a std::vector<pair<QuasiAffineExpr,long>> (the affine-expr + coefficient list), call DynamicAPINFO::setActualPattern(SmallVectorImpl<APPair>) (the resolved concrete AP) and DynamicAPINFO::setStaticOffsetPortion(long) (the constant term).
[evidence: disasm 0xf20230 — vector branch mov esi, 3 @0xf202f9 → pelican::Expr::Expr(ExprKind, ctx) @0xf20304, then mov rax, cs:_ZTVN7pelican15IndirectArgExprE_ptr; scalar branch getRegisterByName @0xf205e4, mov esi, 7 @0xf20619 → Expr::Expr @0xf20624, then mov rax, cs:_ZTVN3bir18BirIntRuntimeValueE_ptr; setActualPattern @0xf202d7, setStaticOffsetPortion @0xf204d1/0xf20753, QuasiAffineExpr ctors @0xf2043b/0xf206b3.]
The ExprKind immediates — 3 for the vector/IndirectArgExpr branch and 7 for the scalar/BirIntRuntimeValue branch — are loaded into esi directly before each pelican::Expr::Expr(ExprKind, ctx) call; this is the binary-level proof of the kind-3 / kind-7 minting.
extractDynamicApToStandAlone 0xf1e2c0 asserts isAccessDynamic first ("Compiler Logic Fault: cannot extract dynamic ap from a static access!"), checks the unique_indices (+0x40) / vector_offset (+0x58) coupling ("When unique_indices=True, it's expected that the vector_offset is set. Check the nisa.dma_copy call"), then recurses codegenAccess on the inner offset AP at klr+0x48.
NET. Codegen mints kind-1 (physical, static) directly from the four encoders, and kind-2 (symbolic, dynamic) via
assembleDynamicInfo'spelican::Expr(kind 3 IndirectArg / kind 7 IntRuntime). It never mints kind-3 (register) — that islower_ap's job (§7).
7. The kind-1 / kind-2 / kind-3 lifecycle
This primitive sits at the top of the AP lifecycle that runs codegen → lower_ap → wire encoder.
| stage | who | produces |
|---|---|---|
| codegen (per-op) | codegenAccess[Simple] / codegenBirAccessPattern + addArgument | kind-1 PhysicalAccessPattern (static AP) — the common case, every I-op |
| codegen (dynamic) | assembleDynamicInfo 0xf20230 | kind-2 SymbolicAccessPattern — pelican::Expr kind 3 / kind 7 addrs |
lower_ap (pre-reg-alloc) | LowerAP::convertSymAP and friends | kind-2 → kind-3 RegisterAccessPattern (mints virtual registers from the symbolic exprs) |
| encoder / wire | CoreV*GenImpl | kind-1 (or the resolved actual_ap of a kind-2/3) → ADDR4 / TENSOR1-4D / MXMEM_PATTERN1D / INDIRECT16B |
Pattern.size (1..4) selects TENSOR1/2/3/4D; Pattern[0].step is the partition step; Pattern[1..3].step are the Z/Y/X free strides; the offset becomes the ADDR4 start address; and the DynamicAPINFO.indirect_dim drives the bit-29 indirect descriptors. The codegen primitive on this page is the source of every kind-1/kind-2 operand that flows down this chain.
8. The master algorithm — one screen
What every I-op invokes per tensor operand:
codegen<Op>(klr::Op op):
for each tensor slot S (out, in0, in1, …):
InstArg a = codegenTensorRef(op.S): // §1
ref = op.S; // shared_ptr<klr::TensorRef>
if (isAccessDynamic(ref) && !op_allows_dynamic) throw; // §6.1 gate
switch (ref.kind): // TensorRef+0x00
1: acc = ref.inner_access (ref+0x08); // klr::Access
switch (acc.kind): // Access+0x00
4: a = codegenBirAccessPattern(acc): // §3 GENERAL
ML = getMemoryLocationByName(acc.name) ?: codegenTensorName
a.Pattern = codegenAP(copy(acc.list@+0x18)) // §4 {i32,u32}→{i64,i64}
a.partition = acc[+0x10]
a.dtype = acc[+0x68] ? codegenDtype(acc[+0x64]) : ML.dtype
1: a = codegenAccessSimple(acc+0x08): // §2 CONTIGUOUS
ML = getMemoryLocationByName(name) ?: codegenTensorName
a.Pattern = { {step=TensorName[+0x48], num=ML[+0x100][0]} }
a.shape = {1,1,1}; a.partition = 0; a.dtype = ML.dtype
else: throw "Unsupported access type"
4: a = codegenHbm(): // §4 DRAM
ML = F.addMemoryLocation(); tag DRAM; num = ML.bytes / dtype_size
else: assert "Unsupported tensorRef type"
// dynamic-capable ops (e.g. dma_copy): if dynamic, build DynamicAPINFO via
// assembleDynamicInfo → pelican::Expr kind3/kind7 → SYMBOLIC kind-2 AP §6
addArgument<PhysicalAccessPattern>(a) // or addOutput — MINTS the bir AP (kind-1)
// … set AluOp / engine / flags / scalars … addInstruction<bir::Inst…>
9. How this layer lines up with its neighbours
- Value model (BIR Value Model): kind-1
PhysicalAccessPattern/ kind-2SymbolicAccessPattern/ kind-3RegisterAccessPattern;APPair16 B{step,num}; the AP value-object's own data at+0x50after the MI base. Codegen produces exactly the kind-1 strides + kind-2pelicanexprs that struct map describes. TheInstArg+0x18layout and the AP-object+0x50layout are distinct (§5 GOTCHA). - MemoryLocation:
+0x100dims/elem-count ptr (used by Simple);+0x108byte size (used by Hbm ÷ dtype). - pelican Expr: ExprKind 3 =
IndirectArgExpr, ExprKind 7 =BirIntRuntimeValue—assembleDynamicInfomints exactly those (mov esi,3/mov esi,7before each ctor). lower_ap: codegen mints kind-1/kind-2;lower_apconverts kind-2→kind-3 before reg-alloc.- beta3 twin (BirCodeGenLoop AP Builders): same abstract
(step, num)AP model, broadcast = stride-0; but beta3 runs the set-algebra inside its builders whereas this beta2codegenAPis a pure relay. The two are deliberately parallel, not contradictory.
Evidence anchors and limits
| id | Confidence | claim |
|---|---|---|
| G1 | HIGH | klr::BirAccessPattern field roles (+0x10 partition, +0x18 list, +0x40 scalar-offset, +0x58 vector-offset, +0x64/+0x68 dtype) are inferred from the encoder + validator disasm; the byte offsets are read directly, but the klr-struct header was never separately mapped. |
| G2 | HIGH | codegenAccessSimple's num = ML[+0x100][0] value source is read from the body; whether the degenerate 1-dim extent is the partition or the free extent is inferred from the {1,1,1} shape default. |
| G3 | CERTAIN | codegenAP sign-extends step (movsxd) and zero-extends num — negative (reverse) strides survive. |
| G4 | CERTAIN | The kind-1-vs-kind-2 decision is not inside codegenBirAccessPattern; it is the isAccessDynamic gate at the call site plus assembleDynamicInfo. The four encoders always emit physical. |
| G5 | CERTAIN | No byte offset is set in codegen; the ADDR4 byte offset is computed downstream. |
Evidence anchors
codegenTensorRef 0xf18b30 (thunk 0x5ff170)
codegenTensorRefImpl 0xf18960 (thunk 0x61bb20) isAccessDynamic & (a4^1) gate;
kind1→codegenAccess, kind4→codegenHbm;
assert klir_to_bir_codegen.cpp:0x135
codegenAccess 0xf18820 (thunk 0x60bb60) kind4→BirAP / kind1→Simple;
"Unsupported access type encountered "
codegenAccessSimple 0xf186d0 (thunk 0x61e870) ML+0x100 num, TensorName+0x48 step,
shape {1,1,1} hdr 0x400000003, part=0, getDtype(ML)
codegenBirAccessPattern 0xf18430 (thunk 0x60be00) name klr+0x38, list klr+0x18,
codegenAP @0xf1859e, part klr+0x10, dtype klr+0x68?+0x64
codegenAP 0xf13f50 (thunk 0x5f51f0) node+0x10, movsxd step @0xf13f8f,
num @0xf13f8c, grow_pod, "N <= capacity()" SmallVector.h:0x5A
codegenHbm 0xf15060 (thunk 0x621ed0) bytes ÷ qword_1DE98C0[dtype]
isAccessDynamic(BAP) 0xf14030 [ap+0x40] | [ap+0x58]
isAccessDynamic(Access) 0xf16540 Access.kind==4 → inner BAP @ Access+0x10
_validate...XOR 0xf140a0 scalar==vector → assert
extractDynamicApToStandAlone 0xf1e2c0
assembleDynamicInfo 0xf20230 vector: mov esi,3 → IndirectArgExpr; scalar:
getRegisterByName + mov esi,7 → BirIntRuntimeValue;
setActualPattern, setStaticOffsetPortion; "indirectDim is 0"
Constants qword_1DBCE40 = 0x400000000 (SV size0/cap4); qword_1DBCF28 = 0x400000003
(shape size3/cap4); qword_1DE98C0 dtype→bytes = [1,1,2,1,1,1,1,1,4,4,2,2,2,2,4,4,4,4,8,8]
libBIR consumers addArgument<PhysicalAccessPattern> @plt 0x5fcd80;
addOutput @plt 0x5fc5e0; PhysicalAccessPattern::setPattern 0x211d40 (libBIR)
Cross-references: BIR Value Model (7.4 — the minted Argument/AccessPattern value-objects, +0x50 MI layout) · BirCodeGenLoop AP Builders (6.5.14 — the beta3 Penguin twin of this primitive) · Dynamic Access Patterns (7.23 — the DynamicAPINFO companion built by assembleDynamicInfo).