[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 27 commits into
Conversation
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Greptile SummaryThis PR adds row-wise prequantized MXFP8 input and gradient support for grouped GEMM.
Confidence Score: 4/5The PR does not yet appear safe to merge because the previously reported frozen-weight backward and fused-forward failures remain outstanding. Row-wise prequantized tensors still encounter unsupported frozen-weight states: the fused forward can save rowwise-only activation storage through a helper that requires columnwise data, while the ordinary and scaled-bias backward scenarios have not been made valid end to end. Files Needing Attention: transformer_engine/pytorch/ops/basic/grouped_linear.py; transformer_engine/pytorch/ops/fused/grouped_mlp.py Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Row-wise MXFP8 grouped tensor] --> B[group_requantize_inplace]
B --> C[Swizzle row-wise scales]
B --> D{Columnwise needed?}
D -->|Yes| E[Dequantize and requantize columnwise]
D -->|No| F[Keep row-wise storage only]
C --> G[Grouped GEMM]
E --> G
F --> G
Reviews (15): Last reviewed commit: "Merge branch 'main' into mxfp8_input_gro..." | 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 |
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
for more information, see https://pre-commit.ci
|
/te-ci L1 pytorch |
|
|
||
| py::object group_requantize_columnwise_and_swizzle_rowwise_( | ||
| py::handle grouped_x, py::handle columnwise_quantizer, const size_t num_tensors, | ||
| std::optional<at::Tensor> first_dims, DType otype, std::optional<at::Tensor> tensor_offsets, | ||
| bool return_dequantized) { |
There was a problem hiding this comment.
| py::object group_requantize_columnwise_and_swizzle_rowwise_( | |
| py::handle grouped_x, py::handle columnwise_quantizer, const size_t num_tensors, | |
| std::optional<at::Tensor> first_dims, DType otype, std::optional<at::Tensor> tensor_offsets, | |
| bool return_dequantized) { | |
| py::object group_requantize( | |
| py::handle grouped_x, py::handle quantizer, const size_t num_tensors, | |
| std::optional<at::Tensor> first_dims, DType otype, std::optional<at::Tensor> tensor_offsets, | |
| bool return_dequantized) { |
I suggest to make this function generic.
The intent of this function as follows
- If quantizer.rowwise_usage is true and grouped_x has rowwise data/scales, then grouped_swizzle
- If quantizer.columnwise_usage is true and grouped_x has columnwise_data/scales, then grouped_swizzle
- If quantizer.rowwise_usage is true and grouped_x doesnt have rowwise_data/scales, then dequant + requant with swizzle fusion
- If quantizer.columnwise_usage is true and grouped_zx doesnt have columnwise_data/scales, then dequant + requant with swizzle fusion
3 --> is a unrealistic case and we can throw an error in that case as well
Also, we should make sure to gather all swizzling directions once(rowwise, colwise or both rowwise/colwise) and call grouped_swizzle once.
There was a problem hiding this comment.
I'm not sure if we should make this function generic, the purpose of it is to handle the mxfp8 output from dispatch, and we are also talking about make this a fused kernel with dispatch's permutation, that is the major reason to refactor it from python into c++ layer. cc @phu0ngng
There was a problem hiding this comment.
So the thing is, for now it is now serving the purpose of
mxfp8 dispatch --> mxfp8 gemm ready input
But it can be this as well in the future
mxfp8 dispatch --> nvfp4 gemm ready input
Now if your intent is to retain the quantization between input and output
like mxfp8 dispatch --> mxfp8 gemm ready input
or nvfp4 dispatch --> nvfp4 gemm ready input
I am ok with that as well. But that should mentioned in the comment.
group_requantize_columnwise_and_swizzle_rowwise_ --> This as a name seems too specific and encodes too much information in the name of the function which isnt needed. Quantizer already has the information with optimize_for_gemm=True/False which tells whether to swizzle or not and which direction to swizzle. So just keeping the name as "group_requantize" should suffice.
As far as fused kernels are concerned, the special case where fusion is available, only that case can be replaced with the fused kernel
In general, I want to differentiate between use-case of a function and intent of the function. Use-case is dispatch --> gemm-ready handling. But the Intent of the function is --> requantize to the best of the ability. And that means if you have already quantized data along a direction, then to make gemm ready you just need to swizzle it. However if you dont have quantized data along a direction, then to make it gemm ready dequant + quant + swizzle(based on quantizer config)
There was a problem hiding this comment.
Currently in your function 1 and 4 that I mentioned above is handled. We can throw error for 2 and 3 as well based on quantizer config and grouped_x.quantizer config. So I am not asking to implement the generic function. But keep the interface and name of the function generic. So that in future if we want to implement new feature we dont have to change the name and signature of the function
There was a problem hiding this comment.
I see. Renamed to group_requantize and added one additional assert. Currently the function only takes rowwise_data and column quantizer, but we can extend in the future.
| prequantized_mxfp8_input = ( | ||
| with_quantized_compute | ||
| and isinstance(input_, GroupedTensor) | ||
| and isinstance(input_quantizers[0], MXFP8Quantizer) | ||
| and isinstance(input_.quantizer, MXFP8Quantizer) | ||
| ) | ||
| if prequantized_mxfp8_input: | ||
| # GroupedTensor forbids reshape and is already in the canonical | ||
| # (total_tokens, in_features) layout; just validate the shape. | ||
| if input_.dim() != 2 or input_.size(-1) != self.in_features: | ||
| raise ValueError( | ||
| "GroupedTensor input must have shape (total_tokens, " | ||
| f"{self.in_features}), but got {tuple(input_.size())}." | ||
| ) | ||
| total_tokens = input_.size(0) | ||
| else: | ||
| x = maybe_dequantize(input_, dtype).reshape(-1, self.in_features) | ||
| total_tokens = x.size(0) |
There was a problem hiding this comment.
Adding a special case for MXFP8 input is quite hacky. If we try to understand the code, this entire code section is converting the input (previously assumed to be BF16, but now may be grouped MXFP8) to the format needed by GGEMM. We can lift all of this into a clean helper function:
def _convert_input_to_grouped_tensor(input_, ...):
# Do nothing if input is already in expected format
if input_is_in_expected_format:
return input_
# Fast requantize impls
if input_is_mxfp8 and compute_is_mxfp8:
return tex.group_requantize...(x)
if fancy_future_fused_impl_is_available:
return tex.fancy_future_fused_impl(...)
# Fallback: dequantize if needed and group quantize
x = maybe_dequantize(input_)
grouped_x = tex.group_quantize(x, ...)
return grouped_xThere was a problem hiding this comment.
There are four sites that these checks are involved, fwd_grouped_linear (this one), bwd_grouped_linear, fwd_grouped_mlp, bwd_grouped_mlp, the paths have common parts, but also differ a lot, e.g. quantize entry point, handle of dbias, NVFP4 path, fallback paths, I feel if we create a single helper function to handle all cases, the function itself might be complicated with a lot of conditional branches. What are your recommendations?
| # rowwise data for the dgrad GEMM and manufacture the columnwise copy | ||
| # for wgrad. Bias grads are reduced from the dequantized grad below, | ||
| # which is only kept when there is a bias. | ||
| grouped_dy = grad_output.copy() |
There was a problem hiding this comment.
It will be better if we let the group_requantize_columnwise_and_swizzle_rowwise_ API accepts regular 2D inputs or dY, convert to grouped tensor in C++ for less CPU overhead.
There was a problem hiding this comment.
Agreed that It would reduce CPU overheads, however we would loose generality of the function. If we want to use it for NVFP4 later, then we would need to pass amax as well. I think for now, it is better to consume grouped tensor
| grouped_x = input_.copy() | ||
| if weight_requires_grad: | ||
| tex.group_requantize_columnwise_and_swizzle_rowwise_( | ||
| grouped_x, |
There was a problem hiding this comment.
same comment, it would be better if we just pass in plain torch tensor into this API (please refer to our API design for grouped_quantize, and the grouped tensor descriptor is created in C++ for less overhead
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
for more information, see https://pre-commit.ci
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
for more information, see https://pre-commit.ci
|
/te-ci L1 pytorch |
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
for more information, see https://pre-commit.ci
|
/te-ci L1 pytorch |
Signed-off-by: YangFei1990 <feiw@nvidia.com>
|
/te-ci L1 pytorch |
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: