Repository navigation
Replies: 4 comments 6 replies
|
Thanks @FeathBow for the careful write-up and the reproducer. This is exactly the gap #264 exposed, and the plan fits the extension points from #265. Some direction before you start: 1. 2. 3. The ordering pass, and 4. the mbarriers for bulk copies. A CUDA
This is also where we want the compiler to go next:
5. Tests. The most direct check: run 6. Expectations. TileFoundry's codegen is still far from complete. In TileOps, an agent currently transcribes the incomplete TIR into a TileLang implementation. So expect to find more gaps like |
|
Thanks @FeathBow for the detailed plan and for reading the papers. It looks reasonable to me. A few notes: 1. PR size. I did the reorg so that the code structure does not get in the way of development. Size and pace your PRs in the way that suits you. For example, changes inside one package can go in one PR. 2. Marking at
I did not think these through fully in the reorg, so change them as you need. 3. Synchronization and allocation. Automatic synchronization really has to be decided together with memory allocation: the allocator decides which bytes are reused, and that decides where the syncs go. Your A/B offset observation in the sm_89 GEMM is one example. For the first stage, let's keep it simple and treat them as two separate steps: allocate first, then insert the synchronization from that allocation. Later I hope we can study the combined problem together in more depth. |
|
Thanks @FeathBow for checking before you start, and for reading the lowering this closely. Before we pick a pass, I'd like to lay out how we see the problem, because I think it is larger than placing synchronisation points in the TIR. 1. Where this work sitsflowchart LR
H["Scheduled HIR<br/>written by an agent<br/>op=T.copy_async(), buffers=S"]
C["check_program"]
M["analyze(memory)<br/>liveness + allocation<br/>(synchronous model)"]
L["ConvertHIRToTIR<br/>(hir2tir)"]
T["TIR"]
G["CUDA codegen"]
H --> C --> M --> L --> T --> G
So the work is to rewrite the lowering so that it produces asynchronous TIR. A pass that patches the TIR after lowering cannot do all of it, for the reason in the next section. 2. Asynchronous TIR is not sequential TIR plus barriersBelow, simplified, is what finalize writes today for a staged GEMM (left), and the warp-specialised form we eventually want (right). The right side is pseudo-TIR;
The smallest correct change keeps the left structure and adds
Only the lowering decides which mesh runs which statements, how many loops there are, and where the barriers live. That is why we see this as a rewrite of the lowering rather than a pass after it. 3. How to implement this stageThere are two ways, and both seem reasonable to us. Option A: an asynchronous analysis, then a lowering that consumes itflowchart LR
M["analyze(memory)"] --> A["async analysis (new)<br/>per HIR event: bytes, threads,<br/>read/write, completion"]
A --> P["async plan<br/>roles, commits, waits,<br/>barriers, keyed by HIR events"]
P --> L["lowering in the CUDA target<br/>translates the plan"]
The liveness events are in
Option B: do it inside the lowering, CUDA-specific
What both options must respect
Because the lowering knows S and the stage rule, wait counts and phases follow from them directly. We do not need a fixed point over TIR addresses. 4. Where we want to end upflowchart LR
H["Scheduled HIR"] --> M["memory + async analysis<br/>async-aware lifetimes"]
M --> R["resource-constrained solve<br/>II, (stage, cycle) per op,<br/>offsets, roles"]
R --> P["async plan"]
P --> L["async lowering<br/>(built in this stage)"]
5. Smaller points
Which option suits you? We are happy with either for this stage, as long as the plan, or something that can become one, stays between the analysis and the code it produces. The details still need discussion: the format of the plan, how the roles are chosen, and where the barriers are stored, among others. This is not a simple problem, and it will probably take several rounds. Let's settle them here one at a time. |
|
Thanks @FeathBow, the plan sounds good to us: Option A on top of #277, starting from the smallest correct form and refusing what the analysis cannot prove safe, then comparing placements by the synchronisation they need. This is not a simple problem, and it will likely take a few rounds before the pieces settle. We trust you with it. Please keep us posted here as you go, and raise anything that blocks you or needs a decision from us, so that we can work through it together. |
Uh oh!
There was an error while loading. Please reload this page.
Thanks for pointing me to #265. Here's how I'd like to close the gap from #264, where finalised TIR doesn't order its async copies or say how much dynamic shared memory it needs. I've followed the layout in #265's current code, and the reproducer below ran on main before that refactor.
What's missing
loop_outputs.pyshows both problems. It copies a row into shared memory at offset 0 withT.copy_async, then reads it back five lines later withT.copy, with no commit or wait in between. The host entry builds the launch withoutdynamic_smem, so the kernel gets 0 bytes, and the launcher never raises the limit above 48 KiB. The async copy kernels that do run,async_sync,split_stage, the rings intest_staged_ring.pyand the GEMM in #264, add the ordering by hand, and the GEMM setsdynamic_smem=512itself.You can see this without a GPU. On main before #265, the driver below prints
block = [32, 1, 1] dynamic_smem = 0, and the generated kernel callscopy_asyncand thencopyon the same view. The build then fails withidentifier "smem_base" is undefined.smem_baseis declared where the kernel first uses it, which here is inside the first loop, so the second loop can't see it. I'd fix that as well.Driver and output
This is the part of the generated
device_0.cuthat reads the copy.What I'd like to do
insert_default_host_entrysetsdynamic_smemto the bytes the callee's integer smem views reach, including padding and every ring stage. It runs atBEFORE_CODEGEN, so this also covers TIR written by hand. The verifier checks that a launch covers its callee's views and fits the target, and the launcher callscudaFuncSetAttributeabove 48 KiB.ModulePassatPassPoint.AFTER_LOWER, soschedule finalizeruns it right afterConvertHIRToTIR. Before a read of bytes that acp.asyncwrote, the threads that issued the copy commit their group and wait for it withT.cp_async_wait(n), counting only their own groups, and then aT.syncover the issuing and reading threads makes the bytes visible. Before a write reuses smem bytes that other threads have read, the pass adds aT.sync. It works this out from what the TIR already holds, which is the operand effects, smem byte ranges, issuing mesh and stage rings. It follows each thread's commits and waits through branches and loops, and reuses any sync the author already wrote.T.copy_async_bulkwith mbarriers in the same pass, andT.copy_async_tensoronce it has an emitter.How I'd test it
I'd take the kernels that order themselves by hand, remove their syncs, run the pass, and check that their existing tests still pass. The finalised
cp.asyncfixtures undertests/fixtures/schedule/tir/should compile and match their plain programs on the GPU they target.I'll start once #265 has landed. Let me know if you'd like any of this done differently.
All reactions