Range & Loop Semantics: static / affine / sequential range
All symbols, offsets, and source lines on this page apply to
neuronx_cc2.24.5133.0+58f8de22 (cp310 Cython modules). The public iterators live inneuronxcc/nki/language/iterators.cpython-310-*.so(BuildID203a02d9…); the lowering lives inneuronxcc/nki/compiler/backends/neuron/KernelBuilder.cpython-310-*.so, classNeuronCodegen(BuildID9eb1020e…). cp311/cp312 ship byte-identical Cython twins at different addresses. Treat every address as version-pinned.
Abstract
A Python for loop inside a NKI kernel does not survive as a loop by default. During trace time — the abstract-interpretation pass where NeuronCodegen walks the kernel body op-by-op and emits Penguin IR — every loop is resolved into one of exactly two outcomes: it is either fully unrolled (the body is replayed once per iteration, emitting tripcount copies of every instruction into the IR), or it is deferred as a single Penguin loop axis (AffineAxis / DynamicAxis) carrying one symbolic induction variable (LoopVar) that the backend later schedules and tiles. Which outcome you get is decided entirely by which iterator you wrote, not by any dependence analysis.
There are three loop iterators — nl.static_range, nl.affine_range, nl.sequential_range — plus the implicit fourth case, a bare Python range(...). static_range unrolls at trace time and emits no axis. affine_range and sequential_range both emit one deferred axis through the same node factory, create_affine_axis_block; they differ only by an AxisType attribute carried on that node — Affine (reorderable / vectorizable) versus Sequential (ordered / no-reorder). A bare range(...) is rewritten to sequential_range — the conservative, dependency-safe default. This page reconstructs that decision tree from the binary: the trampoline that routes each iterator to NeuronCodegen, the affine_range generator that is the real workhorse, the Affine-vs-Sequential attribute split, the Axis-vs-DynamicAxis static-vs-runtime IR-type split, and the create_affine_axis_block choke point where a Python range becomes a Penguin loop-axis node.
The single most important thing to internalize: the affine/sequential distinction is a scheduler hint encoded as an attribute on one node class, while the static/deferred distinction is a structural fork between "emit N copies" and "emit one axis." Confusing the two is the most common way to misread this subsystem.
| Public iterators | iterators.affine_range (iterators.py:46), sequential_range (:111), static_range (:23), sync_program (:17) |
| Trampoline | each is a 3-line shim → nki_ctx().<name>(*args, **kwds) (affine_range pw @ 0x12230) |
| Real lowering | NeuronCodegen.affine_range generator @ 0x240260 — all four ranges funnel here |
| Sequential = | NeuronCodegen.sequential_range @ 0xa7c30 (py 697) → affine_range(..., axis_type=AxisType.Sequential) |
| Static = | NeuronCodegen.static_range @ 0x72b40 (py 694) → trace-time full unroll, no axis |
| Bare range = | NeuronCodegen.builtin_range @ 0x81900 → opts.trace_time_unroll_builtin_range ? unroll : sequential_range |
| AxisType enum | KernelBuilder .rodata: members map to strings AffineAxis, Sequential, DynamicAxis (+ Default); enum name AxisType |
| Node factory | NeuronCodegen.create_affine_axis_block @ 0x881f0 — kwargs {loop_name_id, lb, ub, stride, it, parent, insert_before, axis_type, attrs} |
| IR target (static) | bir::LoopAxis via InstLoop::setAxis (E16); (runtime) bir::DynamicForLoopAxis via InstDynamicForLoop::setAxis |
| Cross-ref | 5.3 penguin/axis-loop-model · 5.26 penguin/dynamic-for-loop · §6.5.5 range emitters |
1. The two-layer split: iterator surface vs codegen lowering
The NKI loop machinery is physically two ELF modules with a thin trampoline between them.
Layer A — the language surface. iterators.cpython-310-*.so exports the user-visible names: affine_range, sequential_range, static_range, plus the index helpers arange/ds and the SPMD barrier sync_program. Every one of these is a pure dispatch shim. The decompiled affine_range public wrapper (__pyx_pw_…_iterators_5affine_range @ 0x12230) is three operations:
// iterators.affine_range(*args, **kwds) — iterators.py:46 (CONFIRMED)
static PyObject *iterators_affine_range(PyObject *args, PyObject *kwds) {
PyObject *ctx = GetModuleGlobalName("nki_ctx")(); // the active codegen context
PyObject *m = PyObject_GetAttr(ctx, "affine_range"); // bind ctx.affine_range
return PyObject_Call(m, args, kwds); // ctx.affine_range(*args, **kwds)
}
The identical pattern is CONFIRMED for sequential_range ("sequential_range", iterators.py:111), static_range ("static_range", iterators.py:23), and sync_program ("sync_program", iterators.py:17). nki_ctx() returns the process-singleton trace context (the NeuronCodegen instance); see 6.1.2 nki_ctx scopes. So nl.affine_range(n) is exactly nki_ctx().affine_range(n) — all semantics live in NeuronCodegen.<name>; iterators.py carries none.
NOTE — The "active codegen context" the trampoline fetches is the same
NeuronCodegenreferenced throughout Part 5/6 as the trace-time IR builder (it ownsself.builder, the pelican/BIRIRBuilderC++ binding, andself.curstmt, the insertion anchor). The report-strand premise that lowering lives instarfish/penguin/targets/codegen/NkiCodegen.sois a CORRECTION target — see the callout in §6.
Layer B — the lowering. NeuronCodegen (in KernelBuilder.so) holds the four range methods. The nm roster of the control/scope methods places them adjacent: builtin_range (_61), affine_range (_71), static_range (_89), sequential_range (_91), with create_affine_axis_block (_55) as the shared node factory.
2. The decision tree (the one diagram to keep)
Python source inside @nki kernel
┌──────────────┬──────────────┬───────────────┬──────────────┐
nl.static_range nl.affine_range nl.sequential_range range(...) ← what you wrote
│ │ │ │
│ │ │ builtin_range(@0x81900)
│ │ │ opts.trace_time_unroll_
│ │ │ builtin_range ?
│ │ │ ┌────┴─────┐
│ │ │ true false
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────────┐ axis_type= axis_type= (unroll) sequential_range
│ FULL UNROLL │ Affine Sequential │
│ no Penguin axis│ │ │ │
│ body × tripcnt │ └──────┬───────┴─────────────────────────┘
│ emitted now │ ▼
└─────────────────┘ NeuronCodegen.affine_range (gen @0x240260)
│ normalize_range_args → lb,ub,stride,tripcount
│ extract_loop_directives → attrs (pragmas)
│ has_runtime_value(ub)?
┌───────────┴────────────┐
no yes
│ │
ir_type = Axis ir_type = DynamicAxis
│ │
└──────────┬─────────────┘
▼
create_affine_axis_block(loop_name_id, lb, ub,
stride, it, parent, insert_before,
axis_type, attrs) (@0x881f0)
▼
self.builder → bir InstLoop+LoopAxis (static)
| bir InstDynamicForLoop+DynamicForLoopAxis (runtime)
Two orthogonal forks decide the fate of a loop:
- Unroll vs defer (the structural fork):
static_range(andrangeundertrace_time_unroll_builtin_range) take the left branch — the loop body is materializedtripcounttimes at trace time and no axis node is produced.affine_range/sequential_range/rangetake the right branch — one axis node, one symbolicLoopVar. - Affine vs Sequential (the attribute fork, only on the deferred branch): the
axis_typeattribute carried on the same node tells the backend scheduler whether iterations may be reordered/vectorized (Affine) or must run in program order (Sequential).
A third, independent fork — static vs runtime tripcount (Axis vs DynamicAxis) — is decided inside affine_range by has_runtime_value(ub) and is not tied to the affine/sequential choice; see §5.
3. static_range — full unroll at trace time
// NeuronCodegen.static_range(*args, **kwargs) — KernelBuilder.py:694, pw @0x72b40
// (CONFIRMED: thin wrapper, only `self` operand; forwards to the unroll path)
def static_range(self, *args, **kwargs):
# No create_affine_axis_block call, no LoopVar, no AxisType.
# The range object drives a *concrete* Python iteration during the trace:
# for each i in range(...): replay the loop body, emitting that iteration's
# instructions directly into the current scope. tripcount copies result.
...
The docstring (CONFIRMED verbatim from the iterators .so string pool) states the contract precisely:
"Create a sequence of numbers for use as loop iterators in NKI, resulting in a fully unrolled loop. Unlike
affine_rangeorsequential_range, Neuron compiler will fully unroll the loop during NKI kernel tracing. […] Due to loop unrolling, compilation time may go up significantly […]. On-chip memory (SBUF) usage may also go up significantly […]. No loop-level optimizations will be performed in the compiler.static_rangeshould only be used as a fall-back option for debugging purposes whenaffine_rangeorsequential_rangeis giving functionally incorrect results or undesirable performance characteristics."
Consequences, stated plainly for a reimplementer:
- No
LoopAxisis emitted. Downstream passes (K01ColoringAllocatorWithLoop, K18 SW-pipeline ring rotation) never see a loop here — they see a straight-line block oftripcount × |body|instructions. The induction variable is a concrete Pythonint, one distinct value per unrolled copy, not a symbolicLoopVar. - SBUF pressure is multiplicative. Each unrolled iteration that allocates a tile allocates a distinct tile (no buffer reuse across iterations is implied by an axis), which is why the docstring warns about on-chip memory blowup.
- It is a debugging escape hatch, not a performance tool — the binary's own docstring says so.
GOTCHA — Because
static_rangematerializes concrete ints, you can legally use the loop index in places that demand a compile-time constant (e.g. as astatic_range-indexed Python list lookup of tensors). The same code underaffine_rangewould hand you a symbolicLoopVarand fail. This is the only legitimate reason to reach forstatic_rangeoutside of debugging.
4. affine_range and sequential_range — one node, two attributes
Both deferring iterators funnel through one generator. sequential_range is literally affine_range with a different axis_type:
// NeuronCodegen.sequential_range(*args, **kwargs) — KernelBuilder.py:697, pw @0xa7c30
// CONFIRMED disasm: GetAttr(self,"affine_range"); GetModuleGlobalName("AxisType"); GetAttr(.,"Sequential")
def sequential_range(self, *args, **kwargs):
return self.affine_range(*args, name=…, axis_type=AxisType.Sequential)
The disassembly shows the exact operand sequence: bind self.affine_range, fetch the module global AxisType, get its .Sequential member, and call affine_range with it as axis_type. affine_range itself supplies the default axis_type — the Affine (parallel/reorderable) case — when called directly.
4.1 The AxisType attribute and what it means to the scheduler
The AxisType enum is interned in KernelBuilder .rodata. Its members resolve to the strings AffineAxis, Sequential, and DynamicAxis (the enum name AxisType and the member Default are also interned). The affine/sequential split is not two different node classes — it is this one attribute on the same LoopAxis node:
axis_type | Scheduler contract | From docstring (CONFIRMED) |
|---|---|---|
Affine (default) | iterations independent → may be reordered, vectorized, software-pipelined across compute engines within one NeuronCore | "parallel loop iterators … default … when there is no loop carried dependency … allows … additional loop-level optimizations, such as loop vectorization" |
Sequential | inter-iteration dependency respected → conservative, no reorder/vectorize | "sequential loop iterators … should be used when there is a loop carried dependency … informs Neuron compiler to respect inter-loop dependency and perform much more conservative loop-level optimizations compared to affine_range" |
The affine_range docstring also pins down two subtleties a reimplementer will get wrong:
- Associative reductions are not loop-carried dependencies in this model. Multiple
nl.matmul/nisa.nc_matmulcalls accumulating into one PSUM buffer defined outside the loop are still safe underaffine_range— the docstring explicitly carves this out with a worked example. affine_rangedoes not parallelize across NeuronCores. "Since each kernel instance only runs on a single NeuronCore,affine_rangedoes not parallelize different loop iterations across multiple NeuronCores. However, different iterations could be parallelized/pipelined on different compute engines within a NeuronCore." Cross-NeuronCore parallelism is the SPMD launch grid's job (nc()sharding), not the loop iterator's — see 6.1.4 SPMD programming model.
QUIRK — Using
affine_rangewhere a loop-carried dependency genuinely exists is unsafe and "could lead to numerical errors" (CONFIRMED docstring). The compiler does not verify the no-dependency claim —affine_rangeis an assertion by the kernel author, andSequentialis the safe default that a barerangefalls back to (§4.3). The whole point of the attribute is to let the author grant reordering permission the compiler cannot prove on its own.
4.2 The affine_range generator — the workhorse
affine_range is a @contextmanager (a Cython generator, __pyx_gb_…_NeuronCodegen_72generator12 @ 0x240260). The ordered call sequence below is reconstructed from the generator's symbol references (CONFIRMED operands: normalize_range_args, extract_loop_directives, has_runtime_value, Axis, DynamicAxis, create_affine_axis_block, loop_scope, allocation_scope, LoopVar, iv, pred_lt, wrap_expr, opt_level, nullcontext):
// NeuronCodegen.affine_range — generator @0x240260 (CONFIRMED symbol stream)
@contextmanager
def affine_range(self, *args, name=…, axis_type=AxisType.Affine):
lb, ub, stride, tripcount = self.normalize_range_args(*args) # parse range-like bounds
attrs = self.extract_loop_directives() // §4.4 loop pragmas
# ── static-vs-runtime IR-type fork (independent of affine/sequential) ──
if has_runtime_value(ub): # data-dependent upper bound?
ir_type = DynamicAxis # → bir DynamicForLoopAxis
else:
ir_type = Axis # → bir LoopAxis (the canonical static case)
block = self.create_affine_axis_block( // §5 — the choke point
loop_name_id, lb, ub, stride, it,
parent, insert_before,
axis_type, # Affine | Sequential | (Dynamic via ir_type)
attrs) # the loop directives from extract_loop_directives
# ── scope the body under the induction var, inside a fresh allocation region ──
iv = wrap_expr(it) # the symbolic induction value
with self.loop_scope(predicates=[pred_lt(iv, ub)]): // §4.3 — iv < ub guard
with self.allocation_scope(): // SBUF/PSUM lifetime region
yield LoopVar(iv) # ← the value the kernel `for` binds
# (on __exit__ the generator resumes past the yield: region closed)
Notice the body is yielded once: the kernel's for i in nl.affine_range(n): runs its body a single time during the trace, binding i to one symbolic LoopVar. Every instruction emitted in that single pass is parameterized by iv and lands inside the LoopScope region. The backend, not the trace, replays it tripcount times — which is exactly why affine_range keeps compile time and SBUF bounded relative to static_range (the docstrings cite "better compilation time compared to the fully unrolled iterator static_range").
NOTE — the
trivial_loopfast path. The KernelBuilder string pool interns"trivial loop "and the generator referencesOptLevel/nullcontext. Whentripcount == 1the generator takes a degenerate path that uses anullcontextinstead of opening a realLoopScope— a one-iteration loop needs no axis, noiv < ubpredicate, and no allocation region of its own. This avoids emitting aLoopAxiswhose range is[0,1). (CONFIRMED: string +nullcontext/opt_leveloperands; the exact guard condition is STRONG.)
4.3 The iv < ub predicate and loop_scope
The loop_scope context manager (NeuronCodegen_46, generator @ 0x780d0, kind string "LoopScope") is the region opened around the axis body. It carries predicates=[pred_lt(iv, ub)] — the induction-variable bound guard, built by pred_lt (a "less-than" predicate). This is the trace-time form of the loop's continuation condition; it is lowered by the predicate machinery (lower_predicates → lower_simple_predicate → opcode_for_predicate) into a Penguin compare expression over the axis induction variable. The Affine-vs-Sequential attribute and this iv < ub predicate are independent: the predicate bounds the iteration domain; the attribute grants/denies reordering.
4.4 Loop directives (pragmas → axis attrs)
extract_loop_directives (module function, pf @0x80330) parses kernel loop pragmas into the attrs dict passed to create_affine_axis_block. The directive taxonomy is CONFIRMED from KernelBuilder .rodata:
| Directive | Purpose | Error sentinel (CONFIRMED) |
|---|---|---|
AutoPipelineDirective | request SW-pipelining of the axis | "Multiple auto_pipeline directives are not supported!" |
MultiBufferDirective | buffer-ring depth (multi-buffering) | "Multiple multi_buffer directives are not supported!" |
PipelineStageDirective | per-region stage_id / execution_order marker | — |
LexicalScopeDirective | lexical scoping hint | — |
| (opt fields) | opt_level, skip_allocators, accumulated_tripcount, tripcount_expr, num_stages, trace_time_unroll_builtin_range | "Unsupported directive" (catch-all) |
These attrs ride on the LoopAxis and are read by the backend SW-pipeline: MultiBufferDirective → buffer-ring size; AutoPipeline/PipelineStage → stage count (the denominator in K18's Euclidean ModuloExpr = number of physical buffers = stage count); num_stages/accumulated_tripcount feed the unroll/rotation period. (Detailed in §6.5.5, the backend range emitters / pass catalog.)
4.5 Bare range(...) → sequential_range (the conservative default)
A Python range(...) used as a loop iterator inside a kernel is intercepted by builtin_range:
// NeuronCodegen.builtin_range — pw @0x81900 (CONFIRMED control flow)
def builtin_range(self, *args, **kwargs):
if self.opts.trace_time_unroll_builtin_range: # GetAttr(self,"opts"),
return <unroll now, like static_range> # GetAttr(opts,"trace_time_unroll_builtin_range")
else:
return self.sequential_range(*args, **kwargs) # GetAttr(self,"sequential_range")
This is CONFIRMED from the decompiled body: it reads self.opts.trace_time_unroll_builtin_range; on the false branch it fetches and calls self.sequential_range. This grounds the sequential_range docstring verbatim: "Inside a NKI kernel, any use of Python range(...) will be replaced with sequential_range(...) by Neuron compiler."
QUIRK — The bare-range default is
Sequential, notAffine. A reimplementer's instinct might be that an un-annotated loop is "probably parallel" — the binary does the opposite. The conservative choice is correct: a loop the author never thought about must not be silently granted reorder permission it may not have, which would risk the very numerical errors §4.1 warns about. To get reorder/vectorization you must opt in withaffine_range;rangeonly ever gives youSequential(or a full unroll iftrace_time_unroll_builtin_rangeis set globally).
5. create_affine_axis_block — where a range becomes a Penguin loop-axis node
This is the single choke point where the deferred branch turns a Python range into a Penguin loop-axis node. It is a generator (NeuronCodegen_55, __pyx_gb_…_NeuronCodegen_56generator8 @ 0x881f0) that builds a kwargs dict and calls into self.builder (the pelican/BIR IRBuilder):
// NeuronCodegen.create_affine_axis_block — generator @0x881f0 (CONFIRMED)
@contextmanager
def create_affine_axis_block(self, loop_name_id, lb, ub, stride, it,
parent, insert_before, axis_type, attrs):
kwargs = { # exact key set from .rodata + disasm
"loop_name_id": loop_name_id, # the axis/loop name
"lb": lb, "ub": ub, "stride": stride, # iteration domain [lb, ub) step stride
"it": it, # induction-variable handle
"parent": parent, # enclosing axis/region (loop-nest parent)
"insert_before": insert_before, # Axis/Instruction anchor (must be None here —
# sentinel: "unexpected case in create affine
# axis block, insert before should be None")
"axis_type": axis_type, # Affine | Sequential | Dynamic
"attrs": attrs, # loop directives (§4.4)
}
block = self.builder.<makeAffineAxisBlock>(**kwargs) # GetAttr(self,"builder"); call
yield block
The CONFIRMED kwargs key set is exactly {loop_name_id, lb, ub, stride, it, parent, insert_before, axis_type, attrs} (read from the string pool + the wrapper's kwargs construction). The error sentinel "unexpected case in create affine axis block, insert before should be None" is interned verbatim and guards the insert_before anchor.
5.1 Mapping to the BIR LoopAxis family (the static/runtime fork realized)
The axis_type + ir_type selection from §4.2 lands on two distinct BIR node families (cross-ref E16 / 5.3 axis-loop-model, 5.26 dynamic-for-loop):
| trace-time choice | BIR instruction | BIR axis node | setAxis | key fields |
|---|---|---|---|---|
static ub (ir_type=Axis), axis_type ∈ {Affine,Sequential} | InstLoop | bir::LoopAxis | InstLoop::setAxis(string, l, l, l) @ 0x3237f0 | name@+0x48, lb@+0x20, ub@+0x28, stride@+0x30; toJson key "LoopAxis" |
runtime ub (ir_type=DynamicAxis) | InstDynamicForLoop | bir::DynamicForLoopAxis | InstDynamicForLoop::setAxis @ 0x3265a0 | hasRuntimeValue=1; 3× QuasiAffineExpr; toJson key "DynamicForLoopAxis" |
The pivotal observation for a reimplementer: Affine and Sequential produce the same node class (InstLoop+LoopAxis) — the distinction is the axis_type attribute carried on the node, which the scheduler (K01 ColoringAllocatorWithLoop) reads to decide whether the trip iterations may be reordered. Only the static-vs-runtime tripcount distinction (has_runtime_value) changes the node class (LoopAxis vs DynamicForLoopAxis). Three trace-time concepts, two node classes, one shared factory.
6. Adversarial self-verification
The five strongest claims on this page, each re-challenged against the binary:
-
"
sequential_range=affine_rangewithaxis_type=AxisType.Sequential." — CONFIRMED. The decompiledsequential_range(@0xa7c30) body doesGetAttr(self,"affine_range"), then_Pyx_GetBuiltinName/_GetModuleGlobalName(AxisType)followed byGetAttr(., "Sequential"), and callsaffine_rangewith it. Traceback string pinsKernelBuilder.py:697. No node class is created here — it delegates. -
"Bare
range(...)defaults tosequential_range, gated bytrace_time_unroll_builtin_range." — CONFIRMED.builtin_range(@0x81900) readsself.opts.trace_time_unroll_builtin_range(bothGetAttrs present in disasm) and on the false branch fetchesself.sequential_range. Matches the verbatimsequential_rangedocstring "any use of Pythonrange(...)will be replaced withsequential_range(...)." -
"
static_rangeemits no axis — it fully unrolls at trace time." — CONFIRMED (docstring) / STRONG (body). The iterators.sodocstring states "fully unroll the loop during NKI kernel tracing." Thestatic_rangewrapper (@0x72b40) is thin (onlyselfoperand, nocreate_affine_axis_block/LoopVar/AxisTypereferences), consistent with forwarding to a concrete-iteration unroll path rather than the axis-emitting generator. I tag the exact unroll mechanism (Pythonrangeconcrete iteration replaying the body) as STRONG, not byte-pinned to a single instruction. -
"The static-vs-runtime fork (
AxisvsDynamicAxis) is decided byhas_runtime_valueinsideaffine_range, independent ofaxis_type." — CONFIRMED. Theaffine_rangegenerator (@0x240260) at lines 1196–1304 looks up the globalAxis, doesGetAttrStr(., "has_runtime_value"), and on the runtime branch looks upDynamicAxisinstead, before feeding the chosen type intocreate_affine_axis_block. Theaxis_typeargument flows separately fromsequential_range's call site, so the two forks are genuinely orthogonal. -
"
create_affine_axis_blockkwargs are{loop_name_id, lb, ub, stride, it, parent, insert_before, axis_type, attrs}and it is the single choke point." — CONFIRMED. All nine keys are interned in KernelBuilder.rodataalongside the method name and its__pyx_scope_struct_14_create_affine_axis_blockscope; the sentinel "unexpected case in create affine axis block, insert before should be None" confirmsinsert_beforehandling. The affine_range generator's reference tocreate_affine_axis_blockconfirms it is the funnel.
CORRECTION — lowering lives in
KernelBuilder.NeuronCodegen, notNkiCodegen. A naive reading (and some prior P-strand notes) attributes NKI→Penguin loop lowering tostarfish/penguin/targets/codegen/NkiCodegen.so. That is the wrong file.NkiCodegen.sois the reverse direction — a BIR→NKI-Python source-text printer (write_line/begin_loop/ir_to_nki), a debug round-tripper. The live trace-time lowering documented here isneuronxcc/nki/compiler/backends/neuron/KernelBuilder.so, classNeuronCodegen. (KernelBuilder also has a generated twin underneuronxcc/generated/…/KernelBuilder—GeneratedNeuronCodegen— with identical method names.)
NOTE — two
AxisTypeenums at two layers; keep them straight. This page'sAxisTypeis the trace-time NKI-codegen enum (KernelBuilder), whose members map toAffineAxis/Sequential/DynamicAxis(+Default). The penguin/IRAxisTypedocumented in 5.3 axis-loop-model is a different, richer enum —{Default, Parallel, Sequential, Shard, Thread, Block, CCRank, Interleave, SPMD}— on theAxisclass hierarchy (AffineAxis/DynamicAxis/SequentialAxissubclasses). The trace-timeAffinecorresponds to the penguinParallel/AffineAxis(reorderable static loop); the trace-timeSequentialcorresponds to the penguinSequential; the trace-timeDynamiccorresponds to the penguinDynamicAxis. The extra penguin members (Shard/Thread/Block/CCRank/SPMD) are SPMD launch-grid roles that come from the dimension model (6.1.4), not from the three loop iterators on this page. Do not conflate the loop-iterator enum with the grid-axis enum.
Gaps (SPECULATIVE / not byte-pinned)
- The exact pelican C++
IRBuildermethod name behindself.builder.<makeAffineAxisBlock>(the secondGetAttrthe disasm loads dynamically) is not byte-pinned; semantics are fixed by the E16InstLoop/InstDynamicForLoopmapping above. - The precise
tripcount == 1guard condition for the"trivial loop"fast path is STRONG (string +nullcontext/opt_leveloperands present) but the exact comparison is not isolated to a single instruction. - The internal unroll loop of
static_range(concrete Python iteration replaying the body) is inferred from the thin wrapper + docstring; the per-iteration emission is not traced instruction-by-instruction.