按地址连续性为 GPU 访存选择线程布局
Triton 的 Coalesce pass 根据指针连续性、对齐和线程数量选择分块布局,并按需插入布局转换以保持上下游接口。
工程问题#
张量的逻辑形状相同,不代表相邻线程会访问相邻地址。线程分工与地址步幅不匹配时,访问可能分散到更多内存事务;但修改访存布局又不能破坏上下游操作原有的张量布局。
实际实现#
编译器先运行 AxisInfo 分析,从连续性确定维度顺序,再结合可整除字节数、元素宽度、每线程上限及 CTA 内元素数量计算 sizePerThread。对相关同形同序访存综合考虑后构造 BlockedEncoding;随后转换需要分布式布局的操作数并重建访存操作。普通结果类型变化时再转换回原布局;异步 global-to-local copy 保持原有结果类型。
处理流程#
- 分析指针张量各维度的连续性与对齐信息
- 选择优先连续维,并计算受对齐和操作上限限制的每线程元素数
- 结合线程数量及相关访存,构造 BlockedEncoding
- 转换需要更换布局的操作数并重建操作;结果类型发生变化时再恢复原布局
源码片段#
固定版本 · 原文摘录84 llvm::MapVector<Operation *, Attribute> layoutMap;85 int threadsPerWarp = TritonGPUDialect::getThreadsPerWarp(moduleOp);86 moduleOp.walk([&](Operation *curr) {87 Value ptr = getMemAccessPtr(curr);88 if (!ptr)89 return;90 // We only convert `tensor<tt.ptr<>>` load/store91 bool isPtrTensor = false;92 if (auto tensorType = dyn_cast<RankedTensorType>(ptr.getType()))93 isPtrTensor = isa<PointerType>(tensorType.getElementType());94 if (!isPtrTensor)95 return;96 int numWarps = lookupNumWarps(curr);9798 auto tensorType = cast<RankedTensorType>(ptr.getType());99 CGAEncodingAttr cgaLayout = getCGALayout(tensorType.getEncoding());100 SmallVector<int64_t> shapePerCTA = getShapePerCTA(tensorType);101 auto layout =102 buildCoalescedEncoding(axisInfoAnalysis, curr, numWarps,103 threadsPerWarp, cgaLayout, shapePerCTA);104 layoutMap[curr] = layout;105 });只对可识别的指针张量访存选择此布局,按每个操作的 numWarps 和 CTA 形状调用构造函数。
31 auto contiguity = axisInfoAnalysis.getAxisInfo(ptr)->getContiguity();32 SmallVector<unsigned> order = getOrderFromContiguity(contiguity);33 LDBG("order=[" << triton::join(order, ", ") << "]");3435 auto matchesShape = [&refTensorType](const Value &val) {36 auto rttType = dyn_cast<RankedTensorType>(val.getType());37 return rttType && rttType.getShape() == refTensorType.getShape();38 };3940 // The desired divisibility is the maximum divisibility among all dependent41 // pointers which have the same shape and order as `ptr`.42 llvm::SmallSetVector<Operation *, 32> memAccessesSameOrder;43 memAccessesSameOrder.insert(op);44 if (ptr.getDefiningOp()) {45 for (Operation *use : mlir::getSlice(op)) {46 Value val = getMemAccessPtr(use);47 if (!val || !matchesShape(val) || memAccessesSameOrder.contains(use))48 continue;49 auto currOrder = getOrderFromContiguity(50 axisInfoAnalysis.getAxisInfo(val)->getContiguity());51 if (order == currOrder) {由指针连续性确定维度顺序,并寻找同形且同顺序的相关访存;不是仅按张量的末维统一布局。
177unsigned getNumElementsPerThread(Operation *op, SmallVector<unsigned> order,178 ModuleAxisInfoAnalysis &axisInfoAnalysis,179 ArrayRef<int64_t> shapePerCTA) {180 Value val = getMemAccessPtr(op);181 auto ty = cast<RankedTensorType>(val.getType());182 AxisInfo &valInfo = *axisInfoAnalysis.getAxisInfo(val);183 unsigned elemNumBits = getElementBitWidth(ty);184 unsigned elemNumBytes = std::max(elemNumBits / 8, 1u);185 unsigned maxMultipleBytes = valInfo.getDivisibility(order[0]);186 unsigned maxMultiple = std::max(maxMultipleBytes / elemNumBytes, 1u);187 unsigned maxContig =188 std::min(valInfo.getContiguity(order[0]), shapePerCTA[order[0]]);189 unsigned alignment = std::min(maxMultiple, maxContig);190 unsigned maxElementsPerThread = getMaxElementsPerThread(op);191 unsigned currPerThread = std::min(alignment, maxElementsPerThread);192 LDBG("elemNumBytes: " << elemNumBytes193 << ", divisibility: " << maxMultipleBytes194 << ", contig: " << valInfo.getContiguity(order[0])195 << ", maximum: " << maxElementsPerThread196 << ", alignment: " << alignment);197 return currPerThread;198}把地址可整除性换算为元素对齐,再与连续长度、CTA 形状和操作限制取交集,决定每线程可处理的元素数。
76 perThread = std::min<int>(perThread, std::max(numElems / numThreads, 1));77 LDBG("perThread: " << perThread);7879 if (!dyn_cast<triton::LoadOp>(op)) {80 // For ops that can result in a global memory write, we should enforce81 // that each thread handles at most 128 bits, which is the widest82 // available vectorized store op; otherwise, the store will have "gaps"83 // in the memory write at the warp level, resulting in worse performance.84 // For loads, we can expect that the gaps won't matter due to the L185 // cache.86 perThread = std::min<int>(87 perThread,88 getNumElementsPerThread(op, order, axisInfoAnalysis, shapePerCTA));89 }90 SmallVector<unsigned> sizePerThread(refTensorType.getRank(), 1);91 sizePerThread[order[0]] = perThread;92 return BlockedEncodingAttr::get(op->getContext(), refTensorType.getShape(),93 sizePerThread, order, numWarps,94 threadsPerWarp, cgaLayout);限制每线程元素数,并对非普通 load 的写入路径再次收紧上限,最后构造 sizePerThread 与 BlockedEncoding。
1336Operation *convertDistributedOpEncoding(Attribute encoding, Operation *op) {1337 OpBuilder builder(op);1338 // Convert operands1339 SmallVector<Value, 4> newArgs;1340 for (auto &opOperand : op->getOpOperands()) {1341 Value operand = opOperand.get();1342 if (isDistributedOpEncodingOperand(opOperand)) {1343 Type newType = getNewType(operand.getType(), encoding);1344 newArgs.push_back(triton::gpu::ConvertLayoutOp::create(1345 builder, op->getLoc(), newType, operand));1346 } else {1347 newArgs.push_back(operand);1348 }1349 }13501351 // Convert output types1352 SmallVector<Type, 4> newTypes;1353 for (auto t : op->getResultTypes()) {1354 bool isAsync = isa<triton::gpu::AsyncCopyGlobalToLocalOp>(op);1355 newTypes.push_back(isAsync ? t : getNewType(t, encoding));1356 }为需要分布式布局的操作数插入 ConvertLayout,标量等不需转换的操作数原样保留;AsyncCopyGlobalToLocalOp 的结果类型也保持原样。
1358 // Construct new op with the new encoding1359 Operation *newOp = builder.create(op->getLoc(), op->getName().getIdentifier(),1360 newArgs, newTypes, op->getAttrs());13611362 // Cast the results back to the original layout1363 for (size_t i = 0; i < op->getNumResults(); i++) {1364 Value newResult = newOp->getResult(i);1365 if (newTypes[i] != op->getResultTypes()[i]) {1366 newResult = triton::gpu::ConvertLayoutOp::create(1367 builder, op->getLoc(), op->getResult(i).getType(), newResult);1368 }1369 op->getResult(i).replaceAllUsesWith(newResult);1370 }1371 op->erase();1372 return newOp;以选定的操作数与结果类型重建操作,仅在结果类型改变时插入恢复布局的 ConvertLayout,替换使用者后删除旧操作。
片段保持固定提交的连续原文;完整源码与许可证保存在本地。每段说明交代所在分支,省略部分可通过出处查看。
收益与取舍#
工程分析带来的收益
- 让线程分工适应实际地址模式,增加合并访存和向量化的机会
- 维持上下游可见的结果类型与布局,使其他操作不必在同一 pass 中全部重写
付出的代价
- AxisInfo 提供的是编译期可证明的信息;无法证明连续或对齐时,布局选择会更保守
- 布局转换本身可能产生指令或数据交换,最终收益还取决于后续优化、目标硬件和访问模式
- 这是一项局部布局启发式,不保证最少内存事务,也不是 FlashAttention 那样的完整算子分块算法
可比较的选择
保持原始布局不引入额外转换,但原有线程分工可能导致分散访存。
在内核中手动指定访问与布局便于针对固定形状调优,但需要维护更多硬件和形状分支。
版本与适用范围#
对应固定提交的普通指针张量访存布局选择分支。descriptor load/store 使用单独选择函数,本文不把两者混为一谈;原子操作等每线程上限由 getMaxElementsPerThread 进一步限定。convertDistributedOpEncoding 对 AsyncCopyGlobalToLocalOp 保持结果类型,只有类型改变的结果才插入恢复转换,不能认为所有操作两侧都必定产生 ConvertLayout。
参考来源#
源码、官方文档与原始论文lib/Dialect/TritonGPU/Transforms/Coalesce.cpp · CoalescePass::runOnOperation
lib/Dialect/TritonGPU/Transforms/CoalesceUtils.cpp · buildCoalescedEncoding:维度顺序
lib/Dialect/TritonGPU/Transforms/Utility.cpp · getNumElementsPerThread