Skip to content

[RFC][SIMT] Lower C++ element-wise control flow through LLVM predicates/control stack to PTO/Linx 0.59 #79

Description

@zhoubot

Status and dependency

Compiler RFC. Proposed only; no implementation is authorized by this issue.

This is the Linx LLVM companion to
PTO-ISA/pto-spec#189, which
proposes the common PTO/Linx 0.59 architecture:

  • one main GPR, predicate, Local Tile, and Shared Tile register level;
  • no body-local V register file or scalar-import namespace;
  • ordered B.SEQ and iteration-parallel B.PAR execution markers;
  • direct top-level Tile B.SUBVIEW / B.ASSEMBLE operands; and
  • a TileOp virtual assembly layer that folds one physical bundle into one
    existing TileOp mnemonic.

Compiler baseline inspected for this RFC:
9fb4f7aa89ee22541bfae288e2085ada266ffd75.

The compiler may prototype target IR and MachineIR, but final ISA instruction
selection, predicate/EXEC effects, encodings, and object identity remain
blocked on accepted PTO/Linx 0.59 contracts.

Goal

Allow element-wise Tile/vector computations to remain ordinary C++ for
loops with ordinary structured control flow (if, else, nested diamonds,
canonical break/continue) while LLVM lowers them to PTO/Linx 0.59 SIMT
execution without exposing V registers or mask-stack operations in source.

The intended source style is CUDA/SYCL-like in one important respect: C++
expresses scalar control flow for one logical element, while the compiler
classifies uniform versus varying values and constructs the execution-mask and
reconvergence protocol.

Illustrative pointer baseline:

void piecewise(float *out, const float *x, int n) {
  #pragma clang loop linx_simt(enable) linx_schedule(auto)
  for (int i = 0; i < n; ++i) {
    float v = x[i];
    if (v > 0.0f) {
      if (v > 10.0f)
        out[i] = v * 2.0f;
      else
        out[i] = v + 1.0f;
    } else {
      out[i] = -v;
    }
  }
}

The TileOP/API spelling may wrap the operands in a non-addressable Tile view,
but the body must remain the same language-level loop and branches:

template <class DstView, class SrcView>
void tile_piecewise(DstView dst, SrcView src) {
  #pragma clang loop linx_simt(enable) linx_schedule(auto)
  for (int i = 0; i < src.size(); ++i) {
    auto v = src[i];
    if (v > 0)
      dst[i] = v + 1;
    else
      dst[i] = -v;
  }
}

The exact public Tile-view class and pragma spelling are review items. The
required semantic contract is ordinary C++ for/if source, not explicit
predicate intrinsics or hand-written bundle assembly.

Current compiler surface and gap

The repository already contains useful 0.58 bring-up pieces:

  • llvm/lib/Target/LinxISA/LinxISASIMTAutoVectorize.cpp recognizes counted
    loops, affine memory, reductions, selects, and structured inner CFG;
  • it can if-convert simple diamonds and reports
    grouped_layout_requires_exec_mask_save_restore when real grouped
    divergence requires missing mask save/restore support;
  • llvm/include/llvm/IR/IntrinsicsLinx.td defines
    llvm.linx.vblock.launch with twelve scalar bind slots;
  • llvm/lib/Target/LinxISA/LinxISAInstrInfo.td defines
    PSEUDO_VBLOCK_LAUNCH, body-local PSEUDO_V_* instructions, and TileOp
    pseudos;
  • llvm/lib/Target/LinxISA/LinxISABlockify.cpp expands the launch into one of
    BSTART.{MSEQ,MPAR,VSEQ,VPAR}, B.TEXT, repeated B.IOR imports, body-local
    register IDs, dimensions, and BSTOP; and
  • tests such as autovec_ifconverted_diamond.ll,
    autovec_nested_mask_branch.ll, and autovec_inner_mask_branch.ll already
    exercise multi-block element-wise control flow.

That path is not the 0.59 model. In particular:

  • it serializes a generated body into the linx-vblock-body-asm function
    attribute instead of keeping the body as typed IR/MachineIR;
  • llvm.linx.vblock.launch models a second twelve-entry ri* scalar import
    namespace;
  • PSEUDO_V_* carries immediate body-local ri/vt/vu/vm/vn IDs;
  • grouped divergence currently falls back or rejects because there is no
    explicit mask save/restore and reconvergence representation; and
  • the four MSEQ/MPAR/VSEQ/VPAR headers combine capability and order rather
    than lowering to the proposed B.SEQ / B.PAR order contract.

The 0.59 implementation must remove these architectural assumptions rather
than hide them behind new pseudo names.

Proposed source and frontend contract

Ordinary C++ remains canonical

Clang must preserve the ordinary loop/CFG semantics. The user does not write
EXEC updates, predicate-register allocation, reconvergence labels, control
stack pushes, or B.Z/B.NZ.

The initial frontend should support:

  • canonical counted for loops;
  • pointer/span and compiler-recognized non-addressable Tile-view operands;
  • nested reducible if/else diamonds;
  • SSA PHIs at merges;
  • continue and loop-exit tests whose convergence can be proved; and
  • an explicit opt-in loop hint plus an auto policy.

The frontend emits ordinary LLVM CFG plus loop metadata describing only user
intent and schedule policy. It must not claim that a loop is B.PAR-legal.
That proof belongs to LLVM analyses after optimization.

Provisional policy values:

linx_simt(enable | disable)
linx_schedule(auto | seq | par)

par is a request, not permission to miscompile: a loop that cannot satisfy
the complete B.PAR contract receives a stable diagnostic. auto falls back
to B.SEQ unless independence is proved.

Source value classification

  • Uniform numeric values lower to the main GPR state.
  • Uniform booleans lower to ordinary scalar control and do not modify EXEC.
  • Varying numeric values remain Tile/region SSA values and must not be
    materialized in a body-local V register or ordinary main GPR under B.PAR.
  • Varying booleans lower to 32-bit control predicates and EXEC masks.
  • A packed predicate Tile is data, not a control predicate. TCMP/TCMPS
    predicate-Tile results must not be implicitly reinterpreted as P1..P7 or
    EXEC without an accepted explicit conversion.

LLVM IR and control-stack model

Keep generic SSA/CFG, then annotate divergent control

Do not introduce a source-visible stack. Before target control-flow lowering,
LLVM IR remains normal SSA CFG. A new LinxSIMTControlFlow target pass runs
after loop selection/structurization and uses at least:

  • UniformityAnalysis to distinguish uniform and divergent branches;
  • DominatorTree and LoopInfo to find reconvergence and loop joins; and
  • convergence control to prevent duplication or motion across the SIMT
    execution domain.

The design should follow the architecture pattern of AMDGPU
SIAnnotateControlFlow.cpp, which maintains a compiler-side stack of
{reconvergence block, saved mask SSA value} and inserts target intrinsics for
IF, ELSE, IF_BREAK, LOOP, and END_CF. It must not copy AMD wave64, SGPR, or
instruction semantics.

Provisional Linx target intrinsics, with a 32-bit mask carrier, are:

declare {i1, i32} @llvm.linx.simt.if(i32 %condition_mask)
declare {i1, i32} @llvm.linx.simt.else(i32 %saved_mask)
declare i32       @llvm.linx.simt.if.break(i32 %condition_mask,
                                           i32 %broken_mask)
declare i1        @llvm.linx.simt.loop(i32 %broken_mask)
declare void      @llvm.linx.simt.end.cf(i32 %saved_mask)

The exact intrinsic types may become a target extension predicate type, but
the mask width and semantics must stay compatible with the accepted 0.59
predicate/EXEC architecture. These intrinsics are compiler markers; they are
not new portable LLVM semantics or direct promises of one machine opcode.

Abstract control frame

For each divergent structured region LLVM tracks an SSA control frame
equivalent to:

ControlFrame {
  saved_exec;
  remaining_else_mask;
  reconvergence_block;
  loop_break_mask;      // loops only
  loop_continue_mask;   // when required
}

The frame may later reside in predicate registers, main GPRs where legal, or
architectural execution-instance control-stack state. It is not addressable
C++ state and must not silently spill to ordinary program memory unless the
ISA/ABI explicitly permits such a spill.

Uniform branches remain ordinary branches. Divergent IF/ELSE conceptually
performs:

entry_exec = EXEC
then_exec  = entry_exec & condition_mask
else_exec  = entry_exec & ~condition_mask

execute then under then_exec when nonzero
execute else under else_exec when nonzero
reconverge under the surviving join mask

Early exit, break, and continue update live/broken masks rather than merely
restoring the original mask. The pass must reject irreducible or unsupported
control flow instead of guessing a reconvergence point.

Predicate and EXEC lowering

Introduce an explicit PRED32 MachineIR register class/bank for P0..P7 and
EXEC-related mask values:

  • P0 is the architectural all-active constant and is never allocated as a
    writable destination.
  • Comparisons in a divergent region produce a PRED32 value masked by the
    current EXEC as required by the ISA contract.
  • Predicate logical operations remain in PRED32; they must not round-trip
    through a body-local vector register.
  • IF/ELSE/END-CF pseudos have explicit and implicit EXEC uses/defs so liveness,
    scheduling, sinking, and register allocation cannot move effects across a
    mask transition.
  • A uniform scalar comparison remains a scalar i1/GPR condition and must not
    consume P1..P7.
  • Predicate pressure, nesting depth, call preservation, spill/fill, and trap
    preservation must have an explicit policy. If no legal spill exists, the
    compiler rejects over-depth control flow with a stable diagnostic.

Provisional MachineIR pseudos:

LINX_SIMT_IF       condition_pred, else_bb -> saved_pred, EXEC
LINX_SIMT_ELSE     saved_pred, join_bb     -> saved_pred, EXEC
LINX_SIMT_IF_BREAK condition_pred, broken  -> broken
LINX_SIMT_LOOP     broken, loop_bb         -> EXEC
LINX_SIMT_END_CF   saved_pred              -> EXEC

They deliberately mirror the analyzable shape of RDNA SI_IF, SI_ELSE,
SI_LOOP, and SI_END_CF, while lowering to Linx-specific predicate and
execution-instance operations.

B.Z and B.NZ test the current EXEC mask only after PTO/Linx 0.59 assigns
that meaning. They are not general predicate-register branches. A schematic
divergent branch may use:

P.CMP ... ->P1          # exact comparison mnemonic TBD by ISA
EXEC.SAVE.AND P1 ->P2   # exact mask-update carrier TBD by ISA
B.Z .Lelse_or_join
...
EXEC.ELSE P2
B.Z .Ljoin
...
EXEC.RESTORE P2

These spellings are explanatory, not encoding proposals. LLVM must not emit
them until the owning ISA issue defines the exact carriers and effects.

B.SEQ / B.PAR legality and selection

The current vkind = {MSEQ,MPAR,VSEQ,VPAR} launch field must be replaced by an
orthogonal order policy plus body effect classification.

B.SEQ

  • Main-GPR writes and loop-carried scalar recurrences are legal.
  • LLVM preserves the architectural iteration and dynamic instruction commit
    order.
  • Scalar lowering is allowed when it preserves direct Tile view semantics and
    precise faults.
  • Divergent predicates still require well-formed EXEC/reconvergence state.

B.PAR

  • The complete body inventory must prove no ordinary main-GPR write, including
    untaken paths and compiler-synthesized temporaries.
  • GPR reads use the region-entry snapshot.
  • Varying numeric values must remain Tile/region SSA values.
  • Destination ranges must be proved disjoint unless an accepted atomic or
    reduction contract owns the overlap.
  • Generic loop-carried recurrences reject or select B.SEQ; only explicitly
    supported reductions may use B.PAR.
  • Unknown calls, volatile/atomic effects without an accepted contract,
    irreducible CFG, unknown aliasing, or unresolved predicate spills reject a
    forced par request and force seq under auto.

The pass must emit structured optimization remarks explaining the selected
mode or exact rejection reason.

Direct Tile subview/assemble lowering

For a Tile-view loop, src[i] and dst[i] are compiler-recognized region
operations, not C++ addressable references and not body-local scalar loads.

The selected iteration tuple derives a source region and destination range:

offset = base_offset + LinearIterationID(LC) * fragment_size
  • reads become TileOp source operands carrying the parent Tile identity plus
    B.SUBVIEW range;
  • writes become TileOp destinations contributing a B.ASSEMBLE range to one
    parent working generation;
  • intermediate varying values use temporary top-level Tile generations and
    disjoint regions, not vt/vu/vm/vn registers;
  • simple side-effect-free diamonds may still be if-converted to select/TSEL;
    side-effecting or nested divergent CFG uses the predicate/control-stack
    path; and
  • source snapshots, destination coverage, replay, LAST, fault rollback, and
    parent publication follow the accepted PTO generation contract.

The target IR work should compose with the indexed region/session model in
#74 rather than invent a second range representation.

ISel, MachineIR, Blockify, and virtual TileOp integration

Required compiler restructuring:

  1. Replace linx-vblock-body-asm string serialization with typed IR and
    MachineBasicBlocks for the scheduled body.
  2. Replace llvm.linx.vblock.launch's twelve ri* imports with explicit main
    GPR, predicate, Tile parent, region, and assembly-session SSA operands.
  3. Remove 0.59 use of body-local PSEUDO_V_* immediate register IDs.
  4. Preserve control-flow and predicate pseudos through ISel and register
    allocation with explicit EXEC dependencies.
  5. Make Blockify emit one 0.59 scheduled-region header, B.TEXT, the selected
    B.SEQ/B.PAR marker, dimensions, direct top-level bindings, and one
    boundary.
  6. Lower Tile arithmetic through the existing PSEUDO_TILEOP_* family and a
    generated 0.59 TileOp schema shared by compiler expansion, assembler, and
    bundle-aware disassembler.
  7. Emit B.SUBVIEW and B.ASSEMBLE immediately adjacent to their owning
    binder; never materialize VLOAD/VSTORE copies.
  8. Preserve both explicit-BSTOP and next-BSTART bundle boundaries without
    consuming the next header during virtual disassembly.
  9. Emit the exact PTO ISA 0.59 object note and reject mixed 0.58/0.59 inputs.

Diagnostics and conservative fallback

Provide stable diagnostics/remarks for at least:

  • loop not canonical or trip/range not provable;
  • irreducible or unsupported divergent CFG;
  • reconvergence point not unique;
  • predicate/control-stack depth exhausted;
  • predicate spill required but no legal carrier exists;
  • ordinary GPR write in forced B.PAR;
  • compiler-synthesized GPR temporary makes B.PAR illegal;
  • loop-carried recurrence not an accepted parallel reduction;
  • overlapping or unproved destination assembly ranges;
  • varying value would escape to GPR, ABI, memory, or an unknown call;
  • packed predicate Tile used as a control predicate without conversion;
  • unsupported call, exception, setjmp/longjmp, inline assembly, or volatile
    effect inside the region; and
  • target PTO identity does not provide required predicate/EXEC operations.

auto selects the safe B.SEQ or leaves the loop scalar. A forced par
request must reject rather than silently change ordering.

Minimum test matrix

Clang C++

  • ordinary pointer loop and Tile-view loop with the same for/if body;
  • simple, nested, and empty-arm diamonds;
  • uniform versus varying branch conditions;
  • canonical continue, break, and tail masks;
  • forced seq, forced legal par, forced illegal par, and auto fallback;
  • diagnostics for address-taking/escape of non-addressable Tile element views.

LLVM IR and analysis

  • UniformityAnalysis classification;
  • insertion and nesting of IF/ELSE/END-CF markers;
  • loop IF_BREAK/LOOP mask progression;
  • PHI values at reconvergence;
  • early-exit live-mask behavior;
  • convergence/noduplicate protection against clone, speculation, CSE, and
    sinking across mask transitions;
  • irreducible CFG and control-stack-depth negatives; and
  • no linx-vblock-body-asm string body in the 0.59 lane.

MIR, register allocation, and CodeGen

  • P0 constant behavior and P1..P7 allocation;
  • nested predicate liveness, allowed spills or fail-closed pressure handling;
  • explicit EXEC uses/defs on every control pseudo;
  • no body-local ri/vt/vu/vm/vn class and no VLOAD/VSTORE bridge;
  • B.SEQ GPR recurrence and B.PAR GPR-write rejection;
  • direct Tile parent subview and destination assembly operands;
  • TSEL if-conversion versus real divergent branch cost-model cases; and
  • typed TileOp pseudos expanded through the generated 0.59 schema.

MC/object and runtime

  • integrated Clang assembler plus standalone MC for every new physical form;
  • object/relocation/.note.pto.isa checks for linx32 and linx64;
  • explicit-BSTOP and next-BSTART bundle disassembly;
  • virtual TileOp and physical bundle round trips;
  • runtime nested-divergence sentinels with inactive-lane no-effect checks;
  • exact B.SEQ ordered recurrence results;
  • scheduling-independent B.PAR results and entry snapshots;
  • precise fault/restart at then, else, loop, and reconvergence boundaries; and
  • same-ELF comparison across QEMU, functional model, and RTL/cycle model when
    available.

Proposed implementation stages

  1. Typed-body cleanup: preserve the existing accepted scalar-replay tests
    while replacing string body emission and ri* imports with typed IR/MIR.
  2. B.SEQ control flow: implement uniformity, PRED32, IF/ELSE/END-CF, and
    precise ordered lowering for nested diamonds.
  3. Tile regions: connect C++ Tile views and [RFC][Tile] Lower aligned Tile Array partitions as indexed region/session tokens #74 region/session values to
    direct subview/assemble TileOp pseudos.
  4. B.PAR legality: add whole-body GPR-write/effect preflight and proven
    disjoint-region selection.
  5. Loop control: add break/continue/live-mask/reconvergence and predicate
    pressure policy.
  6. MC and release closure: switch to accepted 0.59 encodings, virtual
    TileOp bundle folding, object identity, cross-model tests, and exact-head
    component pins.

Acceptance boundary

This issue is ready for implementation only after PTO-ISA/pto-spec#189 freezes
the 0.59 execution-instance state, predicate/EXEC operations, B.SEQ/B.PAR
legality, B.Z/B.NZ meaning, and object identity.

Completion requires all of the following:

  • ordinary C++ for/if source reaches typed LLVM and MachineIR;
  • divergent control has explicit analyzable mask/reconvergence state;
  • predicates use the accepted main predicate namespace;
  • no second V/scalar-import register level survives in 0.59 output;
  • B.SEQ and B.PAR are selected and validated under their exact contracts;
  • Tile values access the same top-level register state through direct
    subview/assemble ranges;
  • compiler, assembler, disassembler, emulator, and model agree on one 0.59
    object; and
  • source, IR, MIR, assembly, object-disassembly, and runtime tests cover the
    same representative nested element-wise kernel.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquestionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions