Coalescing and the transpose tax
A free transpose, a slow consumer, and a contiguous copy that may or may not be worth it. Account for the extra segments.
- Module
- The GPU execution model
- Objective
- Explain when a view is free, when the next kernel pays in uncoalesced loads, and when .contiguous() is a measured trade — not a reflex.
Warps load together. Neighbours in the warp should be neighbours in HBM. That is coalescing. A transpose that only rewrites strides does not move bytes at creation time. It can force the next kernel to ask HBM for a strided mess.
Part A — Picture the warp
A row-major float16 matrix M of shape (4096, 4096). Warp of 32 threads.
- Threads read
M[0, 0:32]. How many 32-byte sectors is a best case 64-byte transaction story (order of magnitude: 1–2 transactions vs 32)? - Threads read
M[0:32, 0](a column). Why is that the bad case? T = M.t()as a view. Who pays,t()or the kernel that streamsTas if it were row-major?
Part B — The consumer
a = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)
b = a.t() # view
c = b.contiguous() # copy
y1 = b * 2 # strided
y2 = c * 2 # dense
Predict, then (if you have a GPU) measure with the honest timer:
- Time of
t()vscontiguous()vs each multiply. - Which multiply should be closer to peak GB/s?
- A sentence for the review: ".contiguous() is not free; it is a prepayment." When is the prepayment worth it?
Part C — Gather / embedding
A kernel reads random rows of a (100_000, 4096) fp16 table, batch 8.
- Coalescing: good or bad? Why "random rows" is the whole story.
- Occupancy vs coalescing: which do you suspect first?
- One systems-level mitigation (cache, padding, grouping) in one sentence — no custom CUDA required.
Acceptance
- Row read: coalesced. Column read: many segments. Transpose view defers the tax.
y1slower / lower GB/s thany2;contiguous()cost ≈ one full 32 MiB×2 traffic (read+write of 32 MiB? 4096²×2 = 32 MiB storage; copy reads 32 writes 32).- You do not sprinkle
.contiguous()on every view; you measure the consumer. - Embedding gather: uncoalesced first, occupancy second.
Stretch
Bank conflicts: 32 threads hit the same smem bank. In four sentences, why tiled softmax in shared memory cares, and why this is not the same as HBM coalescing.
Check
a.t() ~0 µs of traffic. contiguous() ~32+32 MiB. Pointwise on strided b can drop to a fraction of peak GB/s. If many consumers follow, pay once. If one tiny consumer follows, maybe live with strides. Gather: each thread's address is a lottery; the bus fetches fat segments for skinny useful bytes.
Debrief
Module 3 is the reason module 1's "transpose is free" came with a warning. The view is free. The access pattern is not.