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

Codegen AccessPattern Primitive — codegenAccess / BirAccessPattern

Every symbol, address, offset, and string on this page applies to neuronx_cc 2.24.5133.0+58f8de22 (cp310), module neuronxcc/starfish/lib/libwalrus.so (ELF64, 64,973,024 bytes; VA == file-offset for .text @ 0x62d660 and .rodata @ 0x1c72000). The same backend is statically linked into nki_klr_sim. Other wheels differ; treat every address as version-pinned. Evidence is the IDA decompiled/disasm sidecars under neuronx-cc/ida/…libwalrus.so/. Note the two-VA-frame artifact: each function has a 6-byte .-prefixed jmp thunk at low .text (0x5f…/0x61…) and the real body at 0xf1…; 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. codegenTensorRefcodegenTensorRefImpl 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).

Modulelibwalrus.so cp310 (class neuronxcc::backend::KlirToBirCodegen)
Dispatch entrycodegenTensorRef 0xf18b30codegenTensorRefImpl 0xf18960
klr::Access switchcodegenAccess 0xf18820 — kind 4 → codegenBirAccessPattern, kind 1 → codegenAccessSimple
General stridedcodegenBirAccessPattern 0xf18430 (664 B) — ML-by-name + list copy → codegenAP
Contiguous fastcodegenAccessSimple 0xf186d0 (331 B) — 1-pair identity AP, shape {1,1,1}
List materialisercodegenAP 0xf13f50 (214 B) — klr::APPair{i32,u32}bir::APPair{i64,i64}, pure relay
DRAM pathcodegenHbm 0xf15060 (629 B) — 2-dim AP, byte-size ÷ dtype-bytes
Dynamic predicateisAccessDynamic(BirAccessPattern) 0xf14030[ap+0x40] | [ap+0x58]
Symbolic builderassembleDynamicInfo 0xf20230 (2683 B) — pelican::Expr kind 3 / kind 7
Transient outputInstArg{MemLoc*, SmallVector<APPair,4>, partition:i32, dtype:i32, valid:u8}
Wire consumersaddArgument<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 == 1codegenAccess, 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 == 4codegenBirAccessPattern, 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 MemoryLocationno 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. codegenAccessSimple lifts num straight out of the MemoryLocation dims pointer at ML+0x100. codegenHbm (§4) cannot — a DRAM MemoryLocation stores its size in bytes, so it divides ML+0x108 by 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

offtypemeaning
+0x00i32kind (== 4 as seen by codegenAccess)
+0x10i32partition / base-index field → InstArg.partition
+0x18ListHeadstd::list<shared_ptr<klr::APPair>> sentinel; nodes walked via [node]
+0x38nametensor name (resolved via getMemoryLocationByName)
+0x40u8scalar-dynamic-offset present (register / unique-indices) → ExprKind 7
+0x58u8vector-dynamic-offset present (gather index) → ExprKind 3
+0x64i32dtype-override index (klr::Dtype, fed to codegenDtype)
+0x68u8dtype-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+0x68codegenDtype(*((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. codegenBirAccessPattern carries no byte offset of its own. The base is the MemoryLocation (resolved by name); the per-dim start is the partition field (klr+0x10) plus the AP geometry. The eventual byte offset (the wire ADDR4 start) is computed downstream by InstBuilder/lower_ap, not here — the body contains no offset store at all.
  • No stride scaling. Each {step, num} passes through codegenAP verbatim; 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 means Pattern[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 so Pattern[0] = partition.
  • No max-of-inputs dtype promotion. Dtype is per-operand: either the explicit per-access cast (klr+0x68 set) or the native MemoryLocation dtype. 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. codegenAP relays 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.

offfieldsource
+0x00MemoryLocation*getMemoryLocationByName (or codegenTensorName fallback)
+0x08SmallVector<APPair,4>.dataself-ptr = &InstArg+0x18 inline buffer
+0x10SmallVector headersize:u32 @+0x10, cap:u32 @+0x14; init 0 / 4
+0x18APPair[4] inline buf4 × 16 B {step:i64, num:i64} (+ access_shape SmallVector on the Simple/Hbm variants)
+0x58i32 partitionklr+0x10; 0 for Simple / Hbm
+0x5Ci32 dtypebir::Dtype — per-access cast or native MemLoc dtype
+0x60u8 validset 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 InstArg SmallVector at +0x18 is not the bir::AccessPattern data at +0x50. This page's transient InstArg is a stack struct local to codegen; its Pattern SmallVector lives at InstArg+0x18. The minted value-object, bir::PhysicalAccessPattern, is a different thing entirely: it is multiple-inheritance — bir::Argument at offset 0 plus a logging::SrcHandle vptr at +0x48, so the AP's own data (the two SmallVector<APPair,4>s) resumes at +0x50 (BIR Value Model §3). Do not conflate the two +0x18/+0x50 layouts: the InstArg is consumed and discarded; the +0x50 layout is what addArgument constructs from it. This page is consistent with 7.4 precisely because it never claims the InstArg is 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 codegenBirAccessPattern will find no kind-2 branch in it at all. The four encoders are unconditionally physical. The static-vs-symbolic fork is the isAccessDynamic(...) & (allow_dynamic ^ 1) gate in codegenTensorRefImpl (which only ever throws on the forbidden case) plus the separate assembleDynamicInfo call the dynamic-capable op makes around addArgument. 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 _ZTVN7pelican15IndirectArgExprE is loaded right after the ctor).
  • Constraint: indirect_dim must 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 (through extractDynamicApToStandAlone).

(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 _ZTVN3bir18BirIntRuntimeValueE loaded 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's pelican::Expr (kind 3 IndirectArg / kind 7 IntRuntime). It never mints kind-3 (register) — that is lower_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.

stagewhoproduces
codegen (per-op)codegenAccess[Simple] / codegenBirAccessPattern + addArgumentkind-1 PhysicalAccessPattern (static AP) — the common case, every I-op
codegen (dynamic)assembleDynamicInfo 0xf20230kind-2 SymbolicAccessPatternpelican::Expr kind 3 / kind 7 addrs
lower_ap (pre-reg-alloc)LowerAP::convertSymAP and friendskind-2 → kind-3 RegisterAccessPattern (mints virtual registers from the symbolic exprs)
encoder / wireCoreV*GenImplkind-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-2 SymbolicAccessPattern / kind-3 RegisterAccessPattern; APPair 16 B {step,num}; the AP value-object's own data at +0x50 after the MI base. Codegen produces exactly the kind-1 strides + kind-2 pelican exprs that struct map describes. The InstArg+0x18 layout and the AP-object +0x50 layout are distinct (§5 GOTCHA).
  • MemoryLocation: +0x100 dims/elem-count ptr (used by Simple); +0x108 byte size (used by Hbm ÷ dtype).
  • pelican Expr: ExprKind 3 = IndirectArgExpr, ExprKind 7 = BirIntRuntimeValueassembleDynamicInfo mints exactly those (mov esi,3 / mov esi,7 before each ctor).
  • lower_ap: codegen mints kind-1/kind-2; lower_ap converts 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 beta2 codegenAP is a pure relay. The two are deliberately parallel, not contradictory.

Evidence anchors and limits

idConfidenceclaim
G1HIGHklr::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.
G2HIGHcodegenAccessSimple'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.
G3CERTAINcodegenAP sign-extends step (movsxd) and zero-extends num — negative (reverse) strides survive.
G4CERTAINThe 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.
G5CERTAINNo 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).