Skip to content

tensorcore support - #9283

Draft
abadams wants to merge 76 commits into
mainfrom
abadams/wmma_intrinsics
Draft

tensorcore support#9283
abadams wants to merge 76 commits into
mainfrom
abadams/wmma_intrinsics

Conversation

@abadams

@abadams abadams commented Aug 5, 2026

Copy link
Copy Markdown
Member

This PR adds support for targeting tensor cores via wmma instructions, unifying that path with our amx support. It adds three new intrinsic families for wmma. They are all pure functions of values that do not touch memory:

wmma_matrix_to_fragment_*: Take a large vector that represents an entire matrix and extract just the lanes that a particular warp lane owns in nvidia's opaque representation. This is effectively a shuffle node, but by an unknown mask.

wmma_fragment_to_matrix_d: Take an wmma fragment owned by a given lane and expand it back into a full matrix, with only the entries known to this lane specified. This is not very useful by itself, but it pairs with the next intrinsic:

wmma_lane_owns: A matrix-sized boolean vector that returns whether or not a given gpu lane knows that matrix entry. Storing a value resulting from wmma_fragment_to_matrix_d with a predicate wmma_lane_owns is an accurate representation of a wmma tile store instructions and is pattern-matched to the same.

The intrinsics also take a bunch of matrix-tile-shape args.

The way these are reached from the front-end is the same as amx support: matrix ops are represented as nested vectorization. However, this PR adds some syntactic sugar for it:

  • MemoryType::AMXTile was renamed MemoryType::Tile, with the old name as a deprecated alias.
  • ::tile_load(x, y) marks a Func as MemoryType::Tile, vectorizes the args, and reorders them innermost
  • ::tile_init(x, y) does exactly the same thing as tile_load. It's really just for readability. Do we even need it?
  • ::tile_store(x, y) just reorders both args innermost and vectorizes them
  • ::tile_matmul(r, x, y) reorders all args innermost, adds .atomic(), and vectorizes all three

With these changes you can get pretty close to cublas performance with reasonable-looking schedules. See the changes to apps/cuda_mat_mul.

Opening as a draft, to get CI coverage while still getting underlying functionality merged. Some of the changes in this branch are already in separate PRs or are going to be carved off into separate PRs.

abadams and others added 30 commits July 30, 2026 11:26
The highest we could target was 8.6, so on anything newer we emitted sm_86
PTX and left it to the driver to JIT. Now Ada, Hopper and both Blackwells
can be named, and host target detection picks them up.

It makes no measurable difference to the tensor core matmul, which is what
prompted this: the kernel only uses instructions that have been available
since 7.0, so there's nothing for a newer target to do. cuBLAS on the same
device runs an sm_80 kernel for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment above halide_target_feature_t lists three places to keep in
sync when adding a feature, and PyEnums.cpp was the one I missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cuda runtime capped kernels at 64 registers per thread when loading a
module, with an HL_CUDA_JIT_MAX_REGISTERS environment variable as an escape
hatch. Capping registers trades spilling against occupancy, and ptxas has more
information about the kernel and the device than we do.

Two apps set the escape hatch to 256 to compensate. Both are removed here; on
an sm_86 device the 256 cap is now slightly slower than letting the driver
decide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_subtile partitions accesses to a tile-memory allocation between the
distinct sub-tiles it holds, and is_load_of_multiramp digs a load out from
under the casts, broadcasts and lane permutations that can wrap it. Neither is
specific to AMX, so move them next to the MultiRamp machinery they are built
on. This drops 126 lines from ExtractTileOperations.

Supporting this, is_multiramp learns to see through a shuffle of a single
vector when the shuffle is a reshaping rather than a gather: either a
transpose, or any permutation of a one-dimensional multiramp whose lane
indices are themselves a multiramp. MultiRamp::transpose applies the former to
a multiramp directly, splitting a dim in two where the transpose falls inside
one.

Routing the AMX operands through is_load_of_multiramp lets that pass match
loads it previously missed, and lets it check the operand types against the
type actually loaded from memory rather than the type of the expression
wrapping it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
is_load_of_multiramp stripped any number of casts off a load and returned only
the Load underneath, so a caller had no way to see that the values had been
cast. ExtractTileOperations then typed its tile registers from the load, and
the signedness of those types picks which of the four integer tdpb
instructions runs. A pipeline multiplying uint8 buffers reinterpreted as int8
compiled to tdpbuud - an unsigned multiply for a signed algorithm.

Peel at most one cast, so the element type of the original Expr and the type
of the returned Load together say whether the values were cast and to what,
and have the AMX integer path reject a mismatch. The float path still reads
the load's type, which is what it wants: the bf16 to f32 widening cast is
expected there, and tile_load takes the bf16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A transpose's shuffle mask is itself a multiramp of constants - transposing
n lanes with c columns gives a mask of shape (n/c, c) and strides (c, 1) - so
the general reshaping-shuffle case in is_multiramp already covers it. Drop the
separate is_transpose case and MultiRamp::transpose, which existed only to
serve it and had no other caller.

That leaves one rule for shuffles of a single vector: it is a reshaping rather
than a gather if the mask is a multiramp of constants, and we can say what it
reshapes to when the shuffled vector is one-dimensional.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dividing a vector by a broadcast only folds when the two are vectorized the
same way, which they aren't when the numerator is a nested vector. Add rules
for the two shapes that come up: a broadcast numerator, where the division can
be pushed inwards until the two line up, and a ramp whose lanes are each
repeated by an inner broadcast, where repeating a lane doesn't change the set
of values so the existing first-and-last-lane argument still applies.

While here, replace the can_prove predicate on the rule being generalized with
a structural one. Matching the base as a multiple of the denominator is enough
to know the quotient is uniform, given the ramp doesn't span far enough to
reach the next multiple. The structural form also generalizes it: the stride
may be any non-negative constant, the base's multiplier need only be a
multiple of the denominator rather than equal to it, and the base may be
affine rather than linear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A store into shared memory whose value is a plain load from global memory
is emitted as an asynchronous copy on sm_80 and later, which moves the
data without routing it through registers. The copies issued in a
producer are waited for at the end of it.

This takes the shared-memory tensor core matmul from 31.4 to 39.5
TFlop/s at 2048^3 on an RTX 5060 Ti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
If nothing between a barrier and the next one at the same level loads
what was stored before it, the later barrier can do the job of both. The
fence types of the elided barrier are added to the one that remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Func stored in GPUSharedAsync goes in GPU shared memory, but is written by
an asynchronous copy instruction that moves the data straight from global
memory without routing it through registers.

Asking for the memory type is a promise that every store to it is a copy the
hardware can make that way, so a store that isn't reports what the copy engine
requires and how a schedule usually satisfies it, rather than quietly falling
back to a load and a store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AMXTile was never added, and GPUSharedAsync is new. List them in the same
order as the enum in Expr.h so that a missing one is easier to spot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cover each of the three copy widths the hardware supports at several element
sizes, plus the shapes a staged input takes: a two-dimensional tile, a padded
stride, two inputs staged into one kernel, and a Func::in wrapper.

The error test checks that breaking each constraint produces a user error
saying which one, rather than crashing or quietly falling back to a load and a
store.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The peephole fired on any store into shared memory that matched, so a Func in
plain GPUShared got an asynchronous copy too, and there was no way to ask for
the synchronous version. That made the memory type only control whether a
store that didn't match was an error, rather than whether the copy was
asynchronous at all.

Require the destination to be GPUSharedAsync, so that GPUShared and
GPUSharedAsync are the two ways to ask for the two lowerings, and a schedule
can compare them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A store whose value wasn't a plain load was always reported as the Func not
being a copy, but that also caught two cases where it is a copy and something
else is wrong. A source read with a stride is broken into a shuffle of dense
loads before it reaches here, and a source computed elsewhere in the kernel is
a load from the wrong place. Report those as themselves.

Also test the predicated case, which needs an align_storage to keep the
destination aligned, or the alignment requirement is what fails first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The density of the two ends of the copy is checked by strided_ramp_base,
whose default stride of one is doing the work. That is easy to misread as
extracting an address, so say so where it is called.

A source read with a stride never reaches that check, because it is broken
into a shuffle of dense loads first and fails the earlier test for being a
plain load. Storing the staged Func in the opposite order to the one it is
read in is what reaches it, so test that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is the GPU counterpart to the existing AMXTile support. Scheduling a
matmul accumulator with .store_in(MemoryType::WMMAAccumulator) makes a
vanilla Halide matrix multiply compile to wmma instructions.

The new pass, extract_wmma_operations, recognizes the three operations that
a tensor core accumulator supports - initialization from zero or from a
matrix in memory, accumulation of a matrix multiply, and copying the result
out - and rewrites them as intrinsics. Anything else is an error, because
the layout of a fragment across the registers of a warp isn't
architecturally specified, so those instructions are the only way in or out.
Nothing in the schedule says the accumulator is spread over a warp, so the
pass also introduces the loop over the 32 lanes.

The fragment intrinsics are pure functions of a matrix value and the lane.
The matrix is named by a Load of the whole of it, with the lanes in
row-major order regardless of how it's laid out in memory, so that the
passes that track uses of an allocation see the access and its true
footprint. Its layout in memory and the distance between its rows or
columns are recovered from the strides of that Load's index. Copying an
accumulator out is a predicated store of the whole matrix, where the
predicate says which entries this lane holds.

Supported: float16 operands, float32 or float16 accumulators, all three tile
shapes, row- and column-major operands and results, several accumulator
fragments per warp, several warps per block, and operands staged through
shared memory.

Along the way:

 - is_multiramp learns to see through the lane permutations the simplifier
   and flatten_nested_ramps introduce, so those passes are free to rewrite
   the accesses and the backend puts them back.
 - The simplifier lifts broadcasts out of pure elementwise calls, so a
   lane-uniform value stays recognizable as one.
 - The subtile partitioning that AMX was doing moves to MultiRamp, shared
   between the two passes.
 - The CUDA runtime no longer caps registers per thread at 64. That was
   costing the tensor core matmul about 1.7x, and cost the two apps that
   set HL_CUDA_JIT_MAX_REGISTERS to work around it.

apps/tensorcore_matmul reaches 40 TFlop/s on an RTX 5060 Ti, against 8 for
the best non-tensor-core schedule and 50 for cuBLAS.

apps/tensorcore_resize ports the block-sparse resampling algorithm, in which
a resize becomes a dense matrix multiply. Its cudaonly schedule works; the
tensorcore one is blocked on a simplifier gap documented in its README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An accumulator scheduled at block level, with the reduction loop above the
loop over warps, is the shape a matmul needs in order to stage its operand
panels into shared memory once per block and have every warp reuse them.
Three things were in the way:

 - The index of an access to the accumulator then depends on which warp is
   doing it. That dependence selects between the per-thread copies of the
   allocation rather than between subtiles within one, so it's substituted
   away before working out which subtile an access refers to.

 - Halide sees an accumulator outside the thread loops as shared between
   threads, so it keeps the atomic node around the update. Codegen then
   scalarizes the store, one lane at a time, and the wmma intrinsics fall
   apart. An accumulator is per-thread register storage, so there's nothing
   to race with, and the pass drops the atomic.

 - The loop over warps was being given the innermost thread dimension, which
   the loop over lanes needs. Inside an accumulator allocation, a thread loop
   is one dimension further out than it looks.

Also tune apps/tensorcore_matmul, which now unrolls a couple of reduction
steps to get more operand loads in flight. Together with the register cap
removal this takes it from 20.7 to 26.1 TFlop/s at 1024, 32.6 to 38.8 at
2048, and 40.2 to 39.6 at 4096, against cuBLAS at 43.3, 49.6 and 50.0.

Staging the operands through shared memory is now expressible, and there's a
test for it, but it isn't yet a win: it peaks at 37.4 TFlop/s against 41.2
for loading the operands straight from global memory and letting L2 do the
reuse. The barrier between staging and computing serializes the two, and
recovering that needs double buffering, which Halide expresses with async
and ring_buffer. Those lower to Fork nodes and semaphores, which have no
meaning in device code, so that's a separate piece of work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e app

The tile shape and the set of sub-tiles were tracked across the whole
pass rather than per allocation, so a pipeline with two accumulators of
different shapes was rejected. apps/tensorcore_resize has one of each,
and now works: on an RTX 5060 Ti it downsamples a 3840x2160 image by 4x
in 0.246 ms against 0.359 ms for the cuda-only schedule.

Both of that app's schedules round the output up to a multiple of the
tile size, so the runner gives them an output buffer of that size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The memory type now covers all three matrices of a multiply, with the role
inferred from use: an allocation accumulated into by a matrix multiply is
the accumulator, and one read as an operand is that operand. An operand
staged this way is loaded into fragment registers where the schedule says
to compute it, and reused by every multiply that reads it, which is a
hoist that nothing downstream can do when the loop isn't unrolled.

The pass now tracks a scope of fragments across two passes over the whole
statement rather than one allocation at a time, so fragments can nest and
several multiplies can be in flight at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things were costing the tensor core matmul instructions it didn't
need to spend.

Each producer waited for its own asynchronous copies, so a block staging
two operand panels paid the latency of the first before it had even
issued the second. Wait instead at the barrier or the load that needs the
data, so every copy a block issues can be in flight at once.

Fragments also round-tripped through their allocations as Halide vectors.
NVPTX holds a wide float vector in pairs of registers, so every tensor
core instruction unpacked its accumulator into eight registers and packed
the result back up again - about 117 instructions per reduction step per
warp, which was most of the instruction stream. Read and write fragments
a register at a time instead, so they never become vectors.

At 2048^3 on an RTX 5060 Ti this takes the best tensor core matmul from
40.0 to 46.0 TFlop/s. cuBLAS is 49.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Without the annotation it has to assume the largest block the hardware
supports and allocate registers for that. Measured no difference on the
tensor core matmul, whose register count is pinned by its live
accumulators, but it is information the backend should be passing on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With fragments no longer packed into and out of vectors, a wider tile per
warp pays off. At 1024^3 / 2048^3 / 4096^3 on an RTX 5060 Ti the tensor
core schedule goes from 27.1 / 39.3 / 40.3 to 31.3 / 45.2 / 46.2
TFlop/s, against cuBLAS at 43.3 / 49.6 / 50.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Of the strides that keep the rows aligned enough for an asynchronous
copy, the one in use was among the worse ones: it left 2.2M shared load
bank conflicts against 23K for the next multiple of eight up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abadams and others added 23 commits August 3, 2026 13:04
The tensor cores multiply brain floats into single precision and eight-bit
integers into 32-bit ones, as well as halves. The pieces that were specific
to halves:

Fragment sizes were two constants. Only the accumulator one is universal -
an accumulator tile holds eight elements per lane whatever its type and
shape. Operand fragments hold exactly their share of the matrix, except
half precision ones, which are a fixed sixteen elements per lane whatever
the shape, with the hardware replicating across lanes when the shape holds
fewer. So the operand size is now derived from the shape and type rather
than assumed.

The element type in the intrinsic names was guessed from the bit width,
which would have silently emitted a half precision instruction for brain
floats. It is now an explicit table that errors on anything unsupported,
and a second table gives the operand and accumulator combinations that
have an instruction.

The intrinsics take a fragment as 32-bit registers. Halide's half vectors
match that signature directly, but brain floats and bytes have to be
bitcast to i32. A fragment that is a single register comes back as that
register rather than as a struct holding one of them.

wmma_matmul covers the new types across every shape, layout, and staging
option it already covered halves for. Its operands are now small integers,
which every type here represents exactly and whose dot products stay exact
in the accumulator, so it checks for equality rather than to a tolerance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shapes were tuned for half precision operands accumulated into single
precision, and the other types were borrowing them. Sweeping sixty shapes
at each size and operand type gives a shape per pair, and the operand type
turns out to matter as much as the size.

Bytes want a tall block spread over four warps, where the 16-bit types
want a wide one over fewer: byte operands make the loads cheap enough to
pay for a much larger accumulator. That is worth 45% at 1024 and 43% at
4096 over the shapes they were borrowing. Brain floats pick out exactly
the same shapes as halves at every size, so they share a row.

Also builds the half-into-half variant, which the app could not previously
ask for. Its operands are sparse zeros and ones so that the dot products
stay under 2048, the largest integer half precision represents exactly,
which keeps the check exact.

The comment now records all seven measured configurations at all three
sizes rather than a couple of ratios.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cublas does brain floats, eight-bit integers, and a half accumulator too,
so the table can compare like with like at every pair of types rather than
leaving the new ones without a reference.

Brain floats land where halves do, within a few percent of cublas, and the
half accumulator is ahead of it at 2048. Eight bit is the weak one: 55% of
cublas at 1024 and 62% at 4096. cublas is not using the wmma instructions
there - eight-bit operands have an mma shape with twice the reduction depth
per instruction, which this schedule cannot reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The staging vector was eight elements, which is the widest asynchronous
copy for a 16-bit operand but half of it for a byte. Making it sixteen
bytes rather than eight elements is the same element-versus-byte
confusion the padding had.

With that fixed, and with the staging depth swept as well - bytes fit
twice the reduction depth in the same shared memory, and mostly want to -
the eight-bit variant goes from 61 to 63 TFlop/s at 1024 and from 81 to
90 at 4096.

That leaves it at 58% to 69% of cublas rather than 55% to 62%. The gap is
still real, but it was not the wmma instructions, as the comment claimed
before this: it was a schedule tuned for 16-bit operands.

The table is now in GFlop/s with a peak column, which shows how far each
row is from what the hardware could do rather than only from cublas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous comment measured the schedules against a peak computed from
the maximum clock and an assumed rate per SM per clock. Both inputs were
wrong: the part averages 2817 MHz while benchmarking rather than its 3090
maximum, and wmma multiplies bytes at the same rate it multiplies halves
into halves, not twice it.

Issuing back-to-back wmma instructions out of registers with no memory
traffic gives the real ceilings: 51541, 99626 and 100650 GOP/s. Against
those the schedules reach 95%, 87% and 89%, and the half-into-half one
matches cublas exactly. That is a much better account of them than 39% of
a peak they could never reach.

It also settles the eight-bit gap, which an earlier commit message got
wrong in both directions. cublas is 29% past the wmma ceiling, so it
cannot be using wmma. The mma instructions reach 188355 GOP/s at the same
shape, 1.87x. Both do the same 8192 ops per instruction - the earlier
claim that the deeper reduction was more work per instruction was wrong -
so what differs is purely how fast the two families issue. The gap is
structural after all, but not for the reason first given.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It reaches 28% of what the cuda cores can do where the tensor core
schedules reach 87% to 95% of their instructions, and the difference is
one missing scheduling primitive rather than an old schedule. Sharing a
staged panel across a block needs the reduction chunk loop above the
thread loops and the accumulator below them, but an accumulator that
outlives the chunk loop lands at block level, where a Register allocation
is sized for the whole block tile and spills. The tensor core schedules
only get around it because a WMMAFragment allocation at block level is
already per-lane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four had gone stale as the generator grew: the header still said only half
precision reached the tensor cores, tiles_x said the best block gets
smaller as the matrices grow, the block shape comment said bytes want four
warps when they want two, and the tensor core schedule quoted ratios
against cublas from before the retune.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main factored the test for which allocations stay in registers into
allocation_goes_to_registers, so the tensor core fragment case moves into
that helper rather than the caller.
Running the filter five times inside the lambda and synchronizing once at
the end amortizes the per-launch synchronization just as well as the
hand-rolled sampling loop did, and leaves the sampling to the helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operands are small integers so the results can be checked exactly,
which also makes them very compressible - three quarters of them are zero.
Measuring against dense random operands instead moves nothing by more than
a couple of percent, on either side of the comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two sides had their own allocations, so they were multiplying
different data - Halide's small integers against cublas's memset pattern.
Fill a Halide buffer, let running the filter copy it down, and pass
halide_cuda_get_device_ptr of it to cublas, so both multiply the same
numbers. That drops the separate cudaMalloc and memset entirely.

Doing that made the two implementations pair up naturally, so each pair
of types is now one function that runs the filter, checks it, times it,
and times cublas beside it. The runner is 64 lines shorter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runner was carrying its own element get and set for buffers of a type
with no C++ equivalent, which is really just bfloat16. Halide already has
usable float16_t and bfloat16_t, and Float16.h already declares
halide_type_of for both, so the buffers can be plain typed ones and the
dispatch goes away.

The cost is linking libHalide into a program that otherwise only needs the
generated code, which a TODO records. Float16.h depends on nothing but
HalideRuntime.h and its implementation touches nothing in the compiler, so
making those definitions inline and shipping the header - or moving the
types to the runtime - would remove the need.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Its beta was one, so it read the output and added to it, which the filters
here do not do. Zero makes the two do the same work, and is worth up to
ten percent to cublas at the widths where the output is large relative to
the operands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ives

MemoryType::AMXTile and MemoryType::WMMAFragment both describe storage for a
matrix tile in whatever form a target's matrix unit keeps one, so merge them
into MemoryType::Tile. AMXTile remains as a deprecated alias. WMMAFragment was
never released, so it is simply removed.

Add tile_init, tile_load, tile_store and tile_matmul to Stage and Func. They
are sugar over the existing directives: each reorders the dimensions that make
up a tile to be innermost and vectorizes them, tile_matmul additionally marking
the stage atomic so that a reduction dimension can be vectorized. Everything
but tile_store also sets the memory type, since those are the ones that produce
a tile. The order of a tile's dimensions is fixed by the instruction rather
than free to schedule, so imposing it here lets schedules drop it: reorder only
permutes the dimensions it names among the positions they already hold, so a
file-local helper names every dimension to move the tile ones inwards.

The same four directives now describe both targets' matrix units, lowering to
tilezero/tileloadd/tdpbf16ps/tilestored on x86 and to the corresponding wmma
operations on CUDA.

Rename test/correctness/tiled_matmul.cpp to amx_matmul.cpp to say which matrix
unit it covers, and register wmma_matmul.cpp in the correctness CMakeLists,
where it was missing.

In apps/cuda_mat_mul, express the paired splits as tile calls, drop the reorder
arguments the directives now supply, and give the tensor core dimensions the
names mmx, mmy and mmr so that Vars are no longer spelled like RVars.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Its schedule needs adjusting for the tile directives, so it will return in a
later PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Own header first, then internal headers, then system headers, with a blank
line between each group. clang-format sorts within blank-line-separated blocks
but does not reorder the blocks, so these had survived reformatting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wmma_matmul's two consumer schedules and most of tiled_matmul_errors were
still spelled out as reorder, atomic and vectorize. In tiled_matmul_errors
that includes the schedule_matmul helper, which is the valid scaffolding nine
scenarios share, so it should read the way such a schedule is meant to be
written.

scenario_no_matmul keeps the desugared spelling. It puts a value that is not a
matrix multiply into tile memory on purpose, which is not something the
directives can express.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The value of each of these depends on which lane of the warp it runs in, but
nothing in their arguments did, so the dependence was invisible in the IR.
wmma_lane_owns was the worst case: its arguments were three constants, so only
its impurity stopped it being lifted out of the loop over lanes, and it would
have been reasonable to make it pure.

The lane goes last, so the argument indices the backend uses are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abadams
abadams marked this pull request as draft August 5, 2026 17:41
abadams and others added 6 commits August 5, 2026 10:47
# Conflicts:
#	test/correctness/CMakeLists.txt
Load::make lost the default for is_streaming when the short forms were added,
so the call that left it off no longer compiles. The short form is what it
wanted anyway: an unpredicated load from an internal buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 3fe9e54)
Both lost the default for is_streaming when the short forms were added, so the
calls that left it off no longer compile. Most of these are unpredicated
accesses to internal buffers, which is what the short form means. The load of a
matrix a tensor core instruction takes a fragment from has a real image and
parameter, so it keeps the long form and passes is_streaming explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant