[Pytorch] Add support for row-wise quanted input for grouped gemm - #3244
[Pytorch] Add support for row-wise quanted input for grouped gemm#3244YangFei1990 wants to merge 6 commits into
Conversation
Signed-off-by: YangFei1990 <feiw@nvidia.com>
| and isinstance(input_quantizers[0], MXFP8Quantizer) | ||
| and isinstance(input_.quantizer, MXFP8Quantizer) |
There was a problem hiding this comment.
Should we also assert input_quantizers[0] == input_.quantizer?
There was a problem hiding this comment.
I think == comparison is too strict. It will compare all value fields, I think things like columnwise_usage or optimize_for_gemm could mismatch? Maybe we should explicit assert dype to be E4M3? But let me know if the dispatch output tensor's quantizer can match all value fields.
| with_columnwise: bool, | ||
| tensor_offsets: Optional[torch.Tensor] = None, | ||
| ) -> None: | ||
| """Prepare a rowwise-only MXFP8 grouped input for grouped GEMM (in place). |
There was a problem hiding this comment.
Looks like this helper actually "prepares" colwise besides rowwise. I think we should make the function name and docstring clearer and more consistent.
| raise ValueError("Pre-quantized MXFP8 grouped input is missing rowwise data.") | ||
| if grouped_x._with_gemm_swizzled_scales: | ||
| raise NotImplementedError( | ||
| "Pre-quantized MXFP8 grouped input must have scales in compact format." |
| scale_shape = ( | ||
| round_up_to_nearest_multiple(total_tokens, 128), | ||
| round_up_to_nearest_multiple(cols // 32, 4), | ||
| ) |
There was a problem hiding this comment.
Do we need to do this while we already have 128 per-expert-alignment in Dispatch? The total tokens should already be divisible by 128.
For hidden size, there will be an assertion in Dispatch to expect % 512 = 0.
Here, I think we can add assertions instead.
|
|
||
| def prepare_prequantized_mxfp8_grouped_input( | ||
| grouped_x: GroupedTensorStorage, | ||
| quantizer: MXFP8Quantizer, |
There was a problem hiding this comment.
Actually, is there a GroupedQuantizer in TE/PyT? If so, we probably need to use it instead.
There was a problem hiding this comment.
I do not see a GroupedQuantizer in Pytorch. I see what used is tex.group_quantize. Let me know if I missed it. Also can you help me understand the benefit of GroupedQuantizer and how it compared with current Pytorch approach?
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Greptile SummaryAdds row-wise prequantized MXFP8 support to grouped GEMM paths.
Confidence Score: 3/5The PR is not yet safe to merge because fused grouped MLP still aborts with frozen expert weights when input or routing gradients are required. The basic grouped-linear fixes now supply dequantized gradients for scaled bias and directly reduce ordinary dbias without a wgrad stage, but the fused forward still omits columnwise activation data when weights are frozen and then asserts that this data exists while saving tensors, preventing the targeted frozen-expert backward flow from running. Files Needing Attention: transformer_engine/pytorch/ops/fused/grouped_mlp.py, transformer_engine/pytorch/utils.py, tests/pytorch/test_grouped_mlp.py Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Row-wise MXFP8 GroupedTensor] --> B[Repack as GroupedTensorStorage]
B --> C[Swizzle row-wise scales]
B --> D{Columnwise copy needed?}
D -->|Yes| E[Dequantize]
E --> F[Columnwise-only requantize]
C --> G[Forward or dgrad grouped GEMM]
F --> H[Wgrad grouped GEMM]
E --> I[Bias and routing-scale reductions]
Reviews (4): Last reviewed commit: "allow bias + frozen weight path in prequ..." | Re-trigger Greptile |
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
|
/te-ci pytorch |
| requires_grad=False, | ||
| with_gemm_swizzled_scales=False, | ||
| ) | ||
| tex.swizzle_scales_for_gemm_(tmp) |
There was a problem hiding this comment.
It feels like we are swizzling as if the activation is a dense tensor, which is only valid is the activation has tokens-per-expert padded to 128 multiple or higher (like 256).
How are we handling the swizzle of columnwise scales?
There was a problem hiding this comment.
Yes this is the precondition, that we require the input dimension (both row and col) to be 128 multiple or higher. While I built in this way, please let me know if we should handle padding here.
There was a problem hiding this comment.
@zhongbozhu I think the group_quantize + swizzle fusion generates the swizzled scales for columnwise.
| ) | ||
|
|
||
|
|
||
| def prepare_prequantized_mxfp8_input_for_gemm( |
There was a problem hiding this comment.
This function needs to be heavily unit tested like here:
| dbias = None | ||
| dequantized = None | ||
| if with_columnwise or with_dbias or with_dequantized: | ||
| dequantized = tex.group_dequantize(grouped_x, TE_DType[dtype]).rowwise_data.view( |
There was a problem hiding this comment.
Does group dequantize work with already swizzled scales? Would be better if we put an assert here.
There was a problem hiding this comment.
We have an assert above in line 177
| fp8_dtype=quantizer.dtype, | ||
| rowwise_data=grouped_x.rowwise_data.view(total_tokens, cols), | ||
| rowwise_scale_inv=grouped_x.scale_inv.view(scale_shape), | ||
| columnwise_data=None, |
There was a problem hiding this comment.
Why is colwise data None if we have if with_columnwise: above?
There was a problem hiding this comment.
When we have colwise_quantizer.optimize_for_gemm = True, the colwise data should be already swizzled in the quantize kernel. We need to manually swizzle for rowwise data because it is generated from NCCL EP dispatch.
| fc1_weight_shape = (fc1_op.out_features, fc1_op.in_features) | ||
| fc2_weight_shape = (fc2_op.out_features, fc2_op.in_features) | ||
| input_ = input_.reshape(-1, fc1_weight_shape[1]) | ||
| if isinstance(input_, GroupedTensor): |
There was a problem hiding this comment.
Overall I think it makes sense to limit the scope of mxfp8 alltoall to the TE fused ops module because it assumes using grouped kernels already so it's fine if we enforce shape alignment on top of it and we can safely ignore the zero padding of mxfp8 scaling factors. However, was NCCL-EP designed to work with TE fused ops only? Sounds like we should insert more assertions elsewhere if TE fused ops module is not used.
There was a problem hiding this comment.
NCCL EP itself is designed to be compatible with both fused and unfused path. We would like to make it general and in mcore I built many support for both paths. When mcore is ready for the grouped tensor path (which I think you are working on?), we can adopt NCCL EP to that path as well.
For mxfp8 specifically, the major connected interface is dispatch (combine takes bf16 input). TE EP dispatch will return GroupedTensor with only rowwise quantized, I think it should work with both paths?
cc @phu0ngng
| ) -> "GroupedTensor": | ||
| """Rowwise-only MXFP8 GroupedTensor with compact scales (FP8 dispatch wire format).""" | ||
| wire_quantizer = MXFP8Quantizer( | ||
| fp8_dtype=tex.DType.kFloat8E4M3, rowwise=True, columnwise=False |
There was a problem hiding this comment.
Recommendation is to use TE_DType instead of tex.DType when constructing quantizers now, could we please do that here?
| # columnwise copy for wgrad. ``scale_bias`` needs the | ||
| # high-precision grad below, so it takes the dequantized tensor | ||
| # instead of the fused dbias. | ||
| grouped_fc2_dy = grouped_storage_from_grouped_tensor(grad_output) |
There was a problem hiding this comment.
There is already copy method defined in GroupedTensorStorage class to achieve this functionality of creating a shallow storage copy. Could we please use that and remove this method?
| grouped_fc2_dy = grouped_storage_from_grouped_tensor(grad_output) | |
| grouped_fc2_dy = grad_output.copy() |
There was a problem hiding this comment.
Same applies in all other places where this method is used,
| ) | ||
| tmp = MXFP8Tensor( | ||
| shape=(total_tokens, cols), | ||
| dtype=dtype, | ||
| fp8_dtype=quantizer.dtype, | ||
| rowwise_data=grouped_x.rowwise_data.view(total_tokens, cols), | ||
| rowwise_scale_inv=grouped_x.scale_inv.view(scale_shape), | ||
| columnwise_data=None, | ||
| columnwise_scale_inv=None, | ||
| quantizer=quantizer, | ||
| requires_grad=False, | ||
| with_gemm_swizzled_scales=False, | ||
| ) | ||
| tex.swizzle_scales_for_gemm_(tmp) | ||
| grouped_x.scale_inv = tmp._rowwise_scale_inv.view(-1) | ||
| grouped_x._with_gemm_swizzled_scales = True |
There was a problem hiding this comment.
I think the better way should be to directly use the grouped_swizzle API which handles variable dims as well.
This would work only if all per expert dims are padded to 128.
| ) | ||
|
|
||
|
|
||
| def prepare_prequantized_mxfp8_input_for_gemm( |
There was a problem hiding this comment.
I am uncomfortable with the complexity of this function.
I think we should define two functions, one for the prepare_rowwise_mxfp8_input for forward pass and preparing_colwise_mxfp8_input for the backward pass.
To be honest I dont think prepare_rowwise_mxfp8_input is even needed, Simply calling grouped_swizzle API on the activation should be enough which can be done directly before the GEMM in grouped_linear.py and grouped_mlp.py. However, the perf of the variable swizzle kernel is not being well studied. So it would be good to evaluate that before making the change.
For colwise, I am assuming the rowwise dequant + quant is well studied in terms of convergence. And so having a sperate function call for that is ideal which also takes care of the dbias computation
| requires_grad=False, | ||
| with_gemm_swizzled_scales=False, | ||
| ) | ||
| tex.swizzle_scales_for_gemm_(tmp) |
There was a problem hiding this comment.
@zhongbozhu I think the group_quantize + swizzle fusion generates the swizzled scales for columnwise.
Description
To support the NCCL EP FP8 dispatch, this PR introduce a capability to allow grouped gemm to take input that is already quantized in row-wise.
Fixes # (issue)
Type of change
Checklist: