Skip to content

[RFC] Compiler-derived Tile regions and assembly sessions #36

Description

@zhoubot

Status

Proposed for RFC review. Not implemented and not compatible with the currently locked LinxISA/PTO 0.58.3 toolchain.

The architecture/compiler contract is under review in LinxISA/linx-isa#187. Staged implementation is tracked by LinxISA/linx-isa#188.

PTO candidate closure is blocked by PTO-ISA/pto-spec#166, which must reconcile the Local B.IOT SizeCode-12 contract/ASL/AVS contradiction.

Problem

The current range surface exposes raw encoding protocol in ordinary C++ templates:

Subview<Parent, SizeCode, Offset, RegSrc>
Assemble<Parent, ParentSizeCode, INIT, LAST, Offset, RegSrc>

This makes users supply values that LLVM can derive from semantic Tile def-use:

  • SrcSelect from the binder-local operand role;
  • RegSrc from normal GPR allocation;
  • uimm11 from affine offset decomposition;
  • SizeCode from the frozen parent/fragment descriptor;
  • INIT/MIDDLE/LAST/INIT_LAST from one assembly session and its commit boundary;
  • modifier adjacency and B.IOT.L from Blockify.

The current implementation is also not a general programming model: Subview is consumed only by TSTORE, Assemble only by TLOAD, and ordinary TileOPs such as TROWMAX/TEXP/TROWEXPAND*/TMATMUL do not accept range operands.

Proposal

C++17 source model

Ordinary users work with source view proxies and destination assembly sessions:

using namespace pto;
using namespace pto::region;

auto src = view(t2)[slice2d{
    all,
    span{4 * g, 4}  // begin, extent
}];

auto out = make_local_assembly<ParentTile>(scope::all_pes);

TROWMAX(
    out[slice2d{all, index{g}}],
    src
);

auto whole = out.commit();

C++ cannot express Python [:, a:b] tokens inside operator[]; the public spelling uses:

struct all_t {};
struct index { uint64_t value; };            // i == [i:i+1]
struct span  { uint64_t begin, extent; };    // half-open [begin, begin+extent)

template<class RowSelector, class ColSelector>
struct slice2d { RowSelector row; ColSelector col; };

Each operand has an independent region expression:

TROWEXPANDMUL(
    out[slice2d{all, span{2 * k, 2}}],
    view(t20)[slice2d{all, span{2 * i, 2}}],
    view(t16)[slice2d{all, index{j}}]
);

There is no implicit equality between i, j, and k.

Local and Shared assembly

Local assembly may use an implicit session only when LLVM proves a unique non-escaping SSA lifetime. The explicit form is always available:

auto out = make_local_assembly<WholeTile>(scope::all_pes);
TADD(out[slice0], view(a)[slice1], view(b)[slice2]);
auto whole = out.commit();

Shared assembly is always explicit:

auto shared_out =
    make_shared_assembly<WholeTile>(shared_parent, producer_scope);

TADD(shared_out[dst_region], lhs, rhs);
auto published = shared_out.commit();

auto consumer = view(published, consumer_scope)[src_region];

Shared object identity, producer mask, and consumer mask are distinct. Allocation freezes the descriptor but not a participant mask. commit() is a compiler marker that derives LAST; it is not a READY instruction. Shared reads still wait for whole_parent_ready && published.

Compiler-facing model

The frontend lowers source views to a non-storable region token and destination writes to a linear assembly session:

%r0 = tile.subview.local %parent, region
%s0 = tile.assembly.begin.local %destination_parent, scope
%s1 = TILEOP ... %r0 -> %s0[region]
%whole = tile.assembly.commit.local %s1

LLVM, not the API user, derives:

Derived field Compiler source
Local vs Shared binder parent type
source0/source1 binder-local generated operation schema
RegSrc/uimm11 affine CELL offset and GPR allocation
writer SizeCode fragment carrier descriptor
ParentSizeCode frozen parent capacity
INIT/LAST linear session order plus commit
modifier adjacency atomic Blockify lowering unit
Shared per-PE offset per-PE GPR evaluation in one convergence domain

All session operations, Shared source views, and region-aware TileOP consumers are convergent/noduplicate and carry one LLVM convergence domain.

V1 restrictions

The first version intentionally stays narrow:

  • rank-2 regions;
  • dynamic affine origin is allowed;
  • extent, dtype, layout, physical/valid shape, parent capacity, scope kind, and participant mask are compile-time constants;
  • straight-line or fully unrolled fixed contribution count;
  • structured session PHI only when parent/scope/descriptor/generation/coverage agree;
  • no session escape, storage, address-taking, select/freeze, unknown call, or cross-function ABI;
  • every executable path from begin reaches exactly one commit.

128-byte CELL rule

A logical result smaller than 128 B is not a direct B.ASSEMBLE writer.

This must be rejected:

TROWMAX(out[slice2d{all, index{g}}], src); // logical result is 64 B

when successive g values would overlap one architectural CELL.

Legalization must first create one real, fully defined 128 B carrier, for example through an exact reviewed TINSERT/packing sequence:

%f0 = TROWMAX ...       // logical 64 B
%f1 = TROWMAX ...       // logical 64 B
%f128 = PACK128 %f0, %f1
%s1 = ASSEMBLY.WRITE %s0[combined_region], %f128

PACK128 is compiler semantic notation, not a proposed PTO instruction. If no assigned operation can prove the packing semantics, compilation fails closed.

Plain C surface

Plain C uses Clang-only opaque builtin types, not linkable C ABI symbols:

linx_tile_region_token_t part =
    __builtin_linx_tile_subview_local(tile, region);

linx_tile_t fragment =
    __builtin_linx_sfu_trowmax_region(part);

linx_tile_assembly_t next =
    __builtin_linx_tile_assembly_write(session, dst_region, fragment);

linx_tile_t whole =
    __builtin_linx_tile_assembly_commit_local(next);

Parent, region-token, and assembly-session types are non-addressable and cannot be stored, copied through ABI, cast, placed in aggregates, or passed through varargs.

Raw compatibility layer

Raw Subview<..., SizeCode, Offset, RegSrc> and Assemble<..., INIT, LAST, ...> forms may remain temporarily under an explicit pto::raw_range or test-only namespace.

They are not the ordinary API and must never be selected as a silent fallback when semantic lowering fails.

Required compiler/API split

This RFC is not implementable as a cosmetic header wrapper over current inline asm. It requires:

  1. exact PTO candidate release/SizeCode closure;
  2. LLVM MC support for B.SUBVIEW/B.ASSEMBLE;
  3. region/session target types and verifier;
  4. generated operation-role/effect-class schema;
  5. phase/coverage analysis;
  6. MachineIR operands preserved through Tile SSA balance;
  7. per-binder contiguous modifier emission in Blockify;
  8. typed Clang builtins;
  9. C++17 zero-semantics wrappers in this repository.

Validation requirements

  • every accepted Tile operation × operand role × generated effect class;
  • Local and Shared parent descriptor legality;
  • binder-local source role, including Shared operation ordinal != SrcSelect;
  • single/first/middle/last and multi-output atomic sessions;
  • B.IOT.L and modifier adjacency;
  • optimizer PHI/clone/CSE/DCE/convergence negatives;
  • sub-CELL pack and rejection cases, including issues [TROWSUM] physical Columns=1 reduction vector is rejected by Tile alignment check #29 and [TROWSUM] Columns=1 full RMSNorm path fails on dtype modifier and sub-128B TSize #32;
  • per-PE Shared source offsets and prior-generation visibility;
  • plain-C and C++17 CodeGen;
  • linx32/linx64 MC, integrated assembler, object, and disassembly;
  • QEMU/model parity and maintained SuperNPUBench softmax/matmul evidence;
  • exact pin/external SHA manifests and LTO/LLD mixed-version rejection.

Review questions

  1. Is view(tile)[slice2d{...}] the preferred source spelling?
  2. Should Local implicit sessions be supported in v1, or require explicit make_local_assembly everywhere?
  3. Should raw range wrappers remain public under pto::raw_range, or become tests-only immediately?
  4. Is rank-2 plus constant extent the right first implementation boundary?
  5. Which existing TileOP should be the first end-to-end region sentinel: TROWMAX, TADD, TLOAD/TSTORE, or TMATMUL?
  6. What is the exact private/deleted LLVM level-one SHA, if different from public bisheng-linx commit 631961c3?

Decision requested

Please review the programming model and answer the questions above. No implementation or TileOP merge should begin until:

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew 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