[SM120] Support mixed FP8 and FP4 scaled dot - #11154
Conversation
2461fc7 to
f1de86f
Compare
|
@ThomasRaoux @lezcano We admit that the non-trivial changes to LL and ldmatrix lowering in this PR might pass the complexity threshold, but I hope that the new |
Enable mixed-precision tt.dot_scaled on SM120 for FP8 and packed FP4 operands in either order. As described in [the PTX doc](https://docs.nvidia.com/cuda/parallel-thread-execution/index.html#warp-level-matrix-instructions-mma), the MMA instruction expects 4 bits to be placed in an unpacked byte as 00xxxx00. The new fp4 mode in [ldmatrix](https://docs.nvidia.com/cuda/parallel-thread-execution/index.html#warp-level-matrix-instructions-ldmatrix) was introduced to help create such register layouts. With `.src_fmt` .b4x16_p64 and `.dst_fmt` .b8x16, it allows copying packed fp4 values in smem into registers in an unpacked form. So a byte xxxxyyyy in smem is copied into two bytes in registers as 0000xxxx, 0000yyyy. Enabling this variant of ldmatrix is the key contribution of this work. The approach relies on the following: - The src SMEM format .b4x16_p64 is what we call `fp4Padded`, originally introduced for sm100 mixed precision. We can reuse the existing infrastructure to efficiently copy packed fp4 values in global memory into padded SMEM via TMA. - To model the unpacked layout 0000xxxx in registers, we introduce a new attribute `fp4Unpacked` to `DotOperandEncoding`. Similarly to `fp4Padded` for SMEM, the logical shape is packed, but the physical storage in registers is doubled. Support for this was added to some linear layout utilities. With `fp4Padded` source and `fp4Unpacked` destination, we can model the new fp4 variant of ldmatrix exactly. - The MMA instruction expects 4 bits to be unpacked as 00xxxx00 while the result of ldmatrix is 0000xxxx. We resolve this quirk at the lowest layer, `MMAV2.cpp`, by manual shifting, so that the rest of Triton can work with the MMA op as if its operand layout was `fp4Unpacked`, for which we have a LinearLayout representation. In terms of lowering and passes, we roughly go through the following stages: - `AccelerateMatmul`: introduces the MMA layouts and FP4 shared-memory staging; the fp4 operand is staged through `fp4Padded` shared memory and loaded into an `fp4Unpacked` register dot operand. - `OptimizeDotOperands`: Extend `FuseTransMMAV3Plus` for sm120 so that `OptimizeDescriptorEncoding` can select an `fp4Padded` encoding for tensor descriptors, just like how it works for sm100 mixed-precision today. - `LocalLoadOpConversion`: if we find `fp4Padded` source and `fp4Unpacked` destination, we emit the new fp4 ldmatrix instruction. If we cannot select ldmatrix for some reason, we fall back to scalar loads. - `DotOpToLLVM/MMAv2`: Take care of the manual shifting, and emit the new mixed-precision mma.sync instruction. Performance on 8k x 8k x 8k matmul, comparing against two baselines: | Path | TFLOPs | Best configuration | |---|---:|---| | BF16 dequantization (main) | **217** | 128×128×64, 4 warps, 3 stages, no TMA | | FP4→FP8 promotion ([PR 10746](triton-lang#10746)) | **488** | 128×128×64, 4 warps, 2 stages, TMA for A/B only | | Native mixed-precision | **691** | 128×128×128, 4 warps, 2 stages, TMA for A/B and scales | For reproducibility, we have used the following autotuning to script to find the optimal configurations: https://gist.github.com/sjoerdmeijer/62efdf1db84c88ae246c0a92d3726599
f1de86f to
e1d5676
Compare
|
Rebased |
lezcano
left a comment
There was a problem hiding this comment.
Why do you need toRegisterElementLinearLayout? Seems quite hacky.
Also stagePackedFp4OperandThroughSmem seems a bit iffy. I would like @ThomasRaoux to have a look at it as well.
| layout = nvidiaDotToLinearLayout(shape, *this); | ||
| } | ||
|
|
||
| if (getFp4Unpacked()) { |
There was a problem hiding this comment.
but this is just used in nvidiaDotToLinearLayout, why modify everything?
There was a problem hiding this comment.
It is currently only used for nvidiaDot, but we think the concept of fp4Unpacked is generic. So we chose not to over-specialize for the NV path.
| // Lower a local_load whose result uses the fp4Unpacked mxfp4 dot encoding. The | ||
| // source must use the fp4Padded NVMMA shared-memory encoding. First try the fp4 | ||
| // ldmatrix instruction; if the source and register layouts are not compatible | ||
| // with that instruction, fall back to scalar loads. | ||
| static LogicalResult lowerFp4UnpackedLocalLoad( | ||
| triton::gpu::LocalLoadOp op, triton::gpu::LocalLoadOp::Adaptor adaptor, | ||
| const LLVMTypeConverter *typeConverter, ConversionPatternRewriter &rewriter, | ||
| const NVIDIA::TargetInfo &targetInfo) { |
There was a problem hiding this comment.
Can't we do this in the generic lowering where we try to use ldmatrix and otherwise we fallback to ld.shared?
| // fp4Unpacked mxfp4 operand: expand the packed bytes into one 8-bit field | ||
| // per e2m1 value as part of the load. | ||
| if (auto dotEnc = dyn_cast<DotOperandEncodingAttr>(dstTy.getEncoding())) | ||
| if (dotEnc.getFp4Unpacked()) | ||
| return lowerFp4UnpackedLocalLoad(op, adaptor, getTypeConverter(), | ||
| rewriter, targetInfo); |
There was a problem hiding this comment.
this feels a bit too specific. Fine if we carry the dotEnc.getFp4Unpacked() through the main path, but we shouldn't write a full different path that will be barely tested as that's too risky.
There was a problem hiding this comment.
The trade off is that the main path will get sm120-specific complexities that you don't want to deal with otherwise. If you are fine with that, we should be able to merge the paths @sjoerdmeijer
|
I will try to answer the high level question first before addressing the inlined ones, and see if you're happy with this.
Each logical i8 tensor element initially contains two FP4 values, in the low and high nibble of a byte.
The short summary and rationale for this function is that we lower the fp4 unpack sequence into a hardware matrix load, and as a bonus we expose it to sw pipelining and TMA. Or in other words, the staging doesn't have pipelining as a goal, but creates this abstraction or transition of fp4Padded shared mem to a local_load with fp4Unpacked=true. |
| // This helper creates a `local_alloc` followed by a `local_load` for a packed | ||
| // mxfp4 input. Thus, it stages a packed input through shared memory, and then | ||
| // creates a fp4Unpacked dot operand. This is an optimisation for SM120. | ||
| static Value stagePackedFp4OperandThroughSmem(Value v, int opIdx, | ||
| RankedTensorType newRetType, | ||
| PatternRewriter &rewriter) { | ||
| // Keep the packed (K) axis contiguous in SMEM for the dot operand. | ||
| Value smem = getSharedMemoryMMAOperand(v, rewriter, opIdx, | ||
| /*allowTranspose=*/false, | ||
| /*fp4Padded=*/true); | ||
| auto vType = cast<RankedTensorType>(v.getType()); | ||
| auto dotEnc = DotOperandEncodingAttr::get(v.getContext(), opIdx, | ||
| newRetType.getEncoding(), | ||
| /*kWidth=*/2u, | ||
| /*fp4Unpacked=*/true); | ||
| auto regType = vType.cloneWithEncoding(dotEnc); | ||
| return LocalLoadOp::create(rewriter, v.getLoc(), regType, smem); | ||
| } |
There was a problem hiding this comment.
Why are we not just doing a convert layout like we do for other cases where the input is in register?
There was a problem hiding this comment.
Does my message in #11154 (comment) answer this?
| // where user is: | ||
| // - WGMMA/MMAv5, or | ||
| // - `ttg.local_load` if the allocation uses an `fp4Padded` shared memory | ||
| // encoding. |
There was a problem hiding this comment.
I think mixing the two is going to be messy, I don't get why this doesn't work the same way other mmav2 cases do
There was a problem hiding this comment.
This is for propagating fp4Padded = true alloc from the MMA operand across transpose, so that OptimizeDescriptorEncoding can select fp4Padded desc encoding. This is exactly what sm100 mixed precision lowering depends on, and we are just extending this flow for local_load user of fp4Padded alloc.
| fp4Unpacked specifies that each byte containing two packed fp4 elements is | ||
| unpacked into two bytes, with each fp4 value occupying bits [3:0] of its byte. |
There was a problem hiding this comment.
the data type is not fp4 though? so what does that mean in practice for the layout?
There was a problem hiding this comment.
It is conceptually similar to SMEM fp4Padded in that the logical tensor is packed in an i8 container but the physical storage is doubled. In this case a byte yyyyxxxx in a logical shape maps to two i8 registers, 0000yyyy and 0000xxxx.
| if not is_mixed: | ||
| pytest.skip("Mixed fp8 x fp4 operands expected here") | ||
| if not PACK_B_ALONG_K: | ||
| pytest.skip("Mixed fp8 x fp4 requires K-packed fp4") |
There was a problem hiding this comment.
why the non-mixed are not being tested? so we don't support the non K-packed at all?
There was a problem hiding this comment.
The ISA requires K packing. We can run this test with MN-pack but we'd be testing the dequantized path. @sjoerdmeijer Let's test that anyway here.
| parser.add_argument("--block-m", type=int, help="override the M tile size") | ||
| parser.add_argument("--block-n", type=int, help="override the N tile size") | ||
| parser.add_argument("--block-k", type=int, help="override the K tile size") | ||
| parser.add_argument("--num-warps", type=int, help="override the number of warps") | ||
| parser.add_argument("--num-stages", type=int, help="override the number of pipeline stages") |
There was a problem hiding this comment.
can we skip the complexity, tutorials are meant to stay as simple as possible
| @@ -1,4 +1,4 @@ | |||
| // RUN: triton-opt --split-input-file %s --verify-diagnostics | |||
| // RUN: triton-opt %s --split-input-file --allocate-shared-memory-nv='compute-capability=120' --convert-triton-gpu-to-llvm='compute-capability=120' --verify-diagnostics | |||
There was a problem hiding this comment.
why would we need to run those passes here, this seems off. This is meant to check the verifier based on some ops
| bool supportLdStMatrixB8() const override { | ||
| return targetFeatures.supportLdStMatrixB8(); | ||
| } | ||
| bool supportsFp4Ldmatrix() const { |
There was a problem hiding this comment.
maybe the ldmatrix changes can be split in a separate PR?
I think this is not the right way to look at it. We shouldn't be writing the code based on how we expect the code to look like for some specific kernels but we should be thinking about the right way to layer the transformations in the compiler without assuming what the kernel would do. For instance there may be cases where the input of the mma is already in register and we don't want it to go through smem (think A100 flash attention cases). I think making this case go through a different flow than other mmav2 cases is not a good direction. |
|
To answer why we are allocating SMEM for fp4 early and why the lowering cannot work like "other mmav2 cases": Our perf results in the PR description demonstrates why using ldmatrix with the new fp4 mode is important for sm120 mixed-prec mma. We started this work based on an assumption: If we get sm120 mixed-precision The primary complexity of this ldmatrix mode is that the input SMEM needs to be
So, in terms of "the right way to layer the transformations in the compiler", we believe our solution is the simplest and the least invasive one - Granted, our solution targets the most common use of SM120 mixed-precision dot where the input comes straight from global memory. It remains functionally correct when the input is already in registers. We are not aware of motivating use cases where a packed fp4 operand in mixed-precision MMA is produced in registers, but we can potentially consider an alternative path for it, for example, the fp4 -> fp8 promotion solution from #10746. Since that is more of an efficiency problem, we want to defer that and keep our changes to the compiler minimal for this first work. |
Enable mixed-precision tt.dot_scaled on SM120 for FP8 and packed FP4 operands in either order.
As described in the PTX doc, the MMA instruction expects 4 bits to be placed in an unpacked byte as 00xxxx00. The new fp4 mode in ldmatrix was introduced to help create such register layouts. With
.src_fmt.b4x16_p64 and.dst_fmt.b8x16, it allows copying packed fp4 values in smem into registers in an unpacked form. So a byte xxxxyyyy in smem is copied into two bytes in registers as 0000xxxx, 0000yyyy. Enabling this variant of ldmatrix is the key contribution of this work.The approach relies on the following:
fp4Padded, originally introduced for sm100 mixed precision. We can reuse the existing infrastructure to efficiently copy packed fp4 values in global memory into padded SMEM via TMA.fp4UnpackedtoDotOperandEncoding. Similarly tofp4Paddedfor SMEM, the logical shape is packed, but the physical storage in registers is doubled. Support for this was added to some linear layout utilities. Withfp4Paddedsource andfp4Unpackeddestination, we can model the new fp4 variant of ldmatrix exactly.MMAV2.cpp, by manual shifting, so that the rest of Triton can work with the MMA op as if its operand layout wasfp4Unpacked, for which we have a LinearLayout representation.In terms of lowering and passes, we roughly go through the following stages:
AccelerateMatmul: introduces the MMA layouts and FP4 shared-memory staging; the fp4 operand is staged throughfp4Paddedshared memory and loaded into anfp4Unpackedregister dot operand.OptimizeDotOperands: ExtendFuseTransMMAV3Plusfor sm120 so thatOptimizeDescriptorEncodingcan select anfp4Paddedencoding for tensor descriptors, just like how it works for sm100 mixed-precision today.LocalLoadOpConversion: if we findfp4Paddedsource andfp4Unpackeddestination, we emit the new fp4 ldmatrix instruction. If we cannot select ldmatrix for some reason, we fall back to scalar loads.DotOpToLLVM/MMAv2: Take care of the manual shifting, and emit the new mixed-precision mma.sync instruction.Performance on 8k x 8k x 8k matmul, comparing against two baselines:
For reproducibility, we have used the following autotuning to script to find the optimal configurations:
https://gist.github.com/sjoerdmeijer/62efdf1db84c88ae246c0a92d3726599
New contributor declaration
I am not making a trivial change, such as fixing a typo in a comment.
I have written a PR description following these
rules.
I have run
pre-commit run --from-ref origin/main --to-ref HEAD.Select one of the following.
/testforlittests/unittestfor C++ tests/python/testfor end-to-end testsFILL THIS IN.Select one of the following.
littests.littests I have added follow these best practices,including the "tests should be minimal" section. (Usually running Python code
and using the instructions it generates is not minimal.)