-
Notifications
You must be signed in to change notification settings - Fork 797
[Pytorch] Add support for row-wise quanted input for grouped gemm #3244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
11e9007
d234cd8
376b94c
782835c
855f480
8ef7e7e
6015c91
49ba134
12f1dbf
14fedf0
e3990dd
c1e4663
a349d0d
a3219de
7c850ef
d8e8b1c
3bbf778
a677627
e890104
76662c2
f2ff2fc
a056c6d
ce2e5a3
8a1c89f
226405d
3032804
791abaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ | |
| import torch | ||
|
|
||
| import transformer_engine_torch as tex | ||
| from ...constants import DType | ||
| from ...constants import DType, TE_DType | ||
| from ...cpp_extensions import general_grouped_gemm, general_grouped_gemm_for_grouped_tensor | ||
| from ...distributed import CudaRNGStatesTracker | ||
| from ...module._common import WeightGradStore | ||
|
|
@@ -48,6 +48,7 @@ | |
| get_dummy_wgrads_for_params, | ||
| get_main_grad_from_param, | ||
| is_quantized_tensor, | ||
| make_columnwise_gemm_quantizer, | ||
| maybe_dequantize, | ||
| validate_or_alloc_output, | ||
| view_main_grad_as_grouped_buffer, | ||
|
|
@@ -1201,6 +1202,11 @@ def _fuser_forward_split_quantize( | |
| out_buffer: Optional[torch.Tensor] = None, | ||
| ) -> tuple[torch.Tensor, tuple[Optional[torch.Tensor], ...]]: | ||
| """Legacy ``tex.split_quantize`` + ``general_grouped_gemm`` flow.""" | ||
| if isinstance(input_, GroupedTensor): | ||
| raise NotImplementedError( | ||
| "Pre-quantized GroupedTensor input is only supported on the " | ||
| "graph-safe grouped-tensor path." | ||
| ) | ||
| num_groups = self.num_groups | ||
| has_bias = self.has_bias | ||
|
|
||
|
|
@@ -1325,11 +1331,46 @@ def _fuser_forward_grouped_tensor( | |
|
|
||
| # Flatten to 2D so the first dim is the total token count. | ||
| original_shape = list(input_.size()) | ||
| x = maybe_dequantize(input_, dtype).reshape(-1, self.in_features) | ||
| total_tokens = x.size(0) | ||
| prequantized_mxfp8_input = ( | ||
| with_quantized_compute | ||
| and isinstance(input_, GroupedTensor) | ||
| and isinstance(input_quantizers[0], MXFP8Quantizer) | ||
| and isinstance(input_.quantizer, MXFP8Quantizer) | ||
|
YangFei1990 marked this conversation as resolved.
Outdated
|
||
| ) | ||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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_x
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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? |
||
|
|
||
| # Build the input GroupedTensor. | ||
| if with_quantized_compute: | ||
| if prequantized_mxfp8_input: | ||
| # Rowwise-only MXFP8 input (e.g. FP8 token dispatch): feed the | ||
| # rowwise data to the forward GEMM as-is, manufacture the | ||
| # columnwise copy needed by the wgrad GEMM, and swizzle the | ||
| # rowwise scales for the GEMM. | ||
| grouped_x = input_.copy() | ||
| if weight_requires_grad: | ||
| tex.group_requantize_columnwise_and_swizzle_rowwise_( | ||
| grouped_x, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment, it would be better if we just pass in plain torch tensor into this API (please refer to our API design for |
||
| make_columnwise_gemm_quantizer(input_quantizers[0]), | ||
| num_groups, | ||
| split_sizes, | ||
| TE_DType[dtype], | ||
| tensor_offsets=base_split_offsets * self.in_features, | ||
| ) | ||
| else: | ||
| # No wgrad, so no columnwise copy is needed. The forward GEMM | ||
| # still requires swizzled rowwise scales. | ||
| tex.grouped_swizzle_for_gemm(grouped_x, rowwise=True, columnwise=False) | ||
| elif with_quantized_compute: | ||
| input_quantizer = input_quantizers[0] | ||
| input_quantizer.set_usage(rowwise=True, columnwise=weight_requires_grad) | ||
| input_quantizer.optimize_for_gemm = True | ||
|
|
@@ -1682,8 +1723,25 @@ def _fuser_backward_grouped_tensor( | |
|
|
||
| # Flatten grad_output to 2D (total_tokens, out_features) | ||
| # to figure out total tokens. | ||
| dy_2d = grad_output.reshape(-1, self.out_features) | ||
| total_tokens = dy_2d.size(0) | ||
| prequantized_mxfp8_grad = ( | ||
| with_quantized_compute | ||
| and isinstance(grad_output, GroupedTensor) | ||
| and isinstance(ctx.grad_output_quantizers[0], MXFP8Quantizer) | ||
| and isinstance(grad_output.quantizer, MXFP8Quantizer) | ||
| ) | ||
| if prequantized_mxfp8_grad: | ||
| # GroupedTensor forbids reshape and is already in the canonical | ||
| # (total_tokens, out_features) layout; just validate the shape. | ||
| if grad_output.dim() != 2 or grad_output.size(-1) != self.out_features: | ||
| raise ValueError( | ||
| "GroupedTensor grad output must have shape (total_tokens, " | ||
| f"{self.out_features}), but got {tuple(grad_output.size())}." | ||
| ) | ||
| dy_2d = None | ||
| total_tokens = grad_output.size(0) | ||
| else: | ||
| dy_2d = grad_output.reshape(-1, self.out_features) | ||
|
vthumbe1503 marked this conversation as resolved.
|
||
| total_tokens = dy_2d.size(0) | ||
|
|
||
| # Build the grad_output GroupedTensor. | ||
| # Optionally get dbias is fusion available with bgrad_group_quantize | ||
|
|
@@ -1700,7 +1758,34 @@ def _fuser_backward_grouped_tensor( | |
| fuse_bgrad = isinstance(grad_output_quantizer, MXFP8Quantizer) or ( | ||
| isinstance(grad_output_quantizer, Float8BlockQuantizer) and ctx.input_requires_grad | ||
| ) | ||
| if has_bias and not self._scale_bias and fuse_bgrad: | ||
| if prequantized_mxfp8_grad: | ||
| # Rowwise-only MXFP8 grad output (e.g. FP8 token dispatch): reuse the | ||
| # 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() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be better if we let the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| if ctx.weight_requires_grad: | ||
| dy_2d = tex.group_requantize_columnwise_and_swizzle_rowwise_( | ||
| grouped_dy, | ||
| make_columnwise_gemm_quantizer(grad_output_quantizer), | ||
| num_groups, | ||
| split_sizes, | ||
| TE_DType[dtype], | ||
| tensor_offsets=base_split_offsets * self.out_features, | ||
| return_dequantized=has_bias, | ||
| ) | ||
| else: | ||
| # No wgrad, so no columnwise copy is needed. Dequantize before | ||
| # swizzling: dequantization reads the unswizzled rowwise scales. | ||
| dy_2d = ( | ||
| tex.group_dequantize(grouped_dy, TE_DType[dtype]).rowwise_data.view( | ||
| total_tokens, self.out_features | ||
| ) | ||
| if has_bias | ||
| else None | ||
| ) | ||
| tex.grouped_swizzle_for_gemm(grouped_dy, rowwise=True, columnwise=False) | ||
| elif has_bias and not self._scale_bias and fuse_bgrad: | ||
| grouped_dy, dbias_packed = tex.bgrad_group_quantize( | ||
| dy_2d, grad_output_quantizer, num_groups, split_sizes | ||
| ) | ||
|
|
@@ -1735,7 +1820,8 @@ def _fuser_backward_grouped_tensor( | |
| offsets=base_split_offsets, | ||
| ) | ||
| elif dbias_packed is None: | ||
| # BF16/FP16 path | ||
| # BF16/FP16 and pre-quantized MXFP8 paths, neither of which fuses dbias | ||
| # into a quantize kernel. | ||
| dbias_packed = compute_grouped_dbias(dy_2d, base_split_offsets, num_groups) | ||
| if self.single_grouped_bias: | ||
| final_bias_grads = [dbias_packed.to(dtype=dtype)] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to make this function generic.
The intent of this function as follows
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Renamed to
group_requantizeand added one additional assert. Currently the function only takes rowwise_data and column quantizer, but we can extend in the future.