grouped linear single param fixes#3178
Conversation
timmoon10
left a comment
There was a problem hiding this comment.
If I understand, this PR fixes 3 bugs:
- The grouped linear module with a single grouped weight does not actually populate the param grad. It computes discrete weight grads, but it never ended up in the single weight param. The fix is to wrap the conversion from single grouped weights to discrete weights within an autograd function.
- The grouped linear module with a single grouped weight was not handling high-precision weight caching (used in Mcore DDP/FSDP). We were caching high-precision weights for the discrete weights, but they were not registered in the single weight param. The fix is to concat the discrete high-precision values and register it in the single weight param.
- When constructing a grouped linear op with
device="meta", it might incorrectly set some Mcore-specific param attrs before the params are initialized. Mcore does this when it constructs an internal fused to replace the grouped linear module implementation (see here).
This is all hairy and messy, but the fixes seem reasonable. It seems that if you enable single grouped weights or any Mcore-specific optimizations, then grouped linear becomes extremely delicate. Users should avoid unless they know what they are doing.
Preserve high-precision MXFP8 initialization when packing expert weights and connect legacy per-GEMM views to the registered grouped parameter for autograd and fused wgrad accumulation. Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Centralize the temporary getter/clearer attachment used by quantized parameter initialization and grouped weight packing. Strengthen MXFP8 tests with discrete-to-grouped FP32 master parity and partial-metadata rejection. Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Use TE's existing cached dummy-wgrad mechanism to trigger the grouped parent hook after fused accumulation without allocating and zeroing a full parameter-sized tensor on every backward. Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
1bfd82a to
b3a6202
Compare
for more information, see https://pre-commit.ci
Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Greptile SummaryThis PR fixes grouped linear behavior for the single grouped-parameter path. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (4): Last reviewed commit: "[pre-commit.ci] auto fixes from pre-comm..." | Re-trigger Greptile |
| # A meta-device op may be created as a parameterless shell and have its | ||
| # grouped parent attached after construction. In that case there is no | ||
| # ``weight`` parameter to mark yet. | ||
| weight = self._parameters.get("weight") |
There was a problem hiding this comment.
without this this, will there be runtime errors in actual training? how do we trigger it?
There was a problem hiding this comment.
Yes. The real underlying issue is lack of explicit API support on the TE side for the MCore pattern of initializing ops with device=meta and then injecting the module weights later. Replicating the issue requires
USE_TE_OPS=True
MOE_SINGLE_GROUPED_WEIGHT=True
NVTE_GROUPED_LINEAR_SINGLE_PARAM=1
DELAY_WGRAD_COMPUTE=True
NVTE_CUTEDSL_FUSED_GROUPED_MLP=1
The order is:
- TE GroupedLinear OP created by MCore with device meta
- It created placeholder parameters, but
weightis not created yet - This part of the code before the changes calls self.weight.skip_backward_post_hook = True, but weight does not exist yet.
In the current code, self._parameters.get("weight") will return None in this case, self.weight wont be accessed. In the "regular" case where device=meta is not used, it will work as normal.
| for index, weight in enumerate(weight_tensors): | ||
| if grouped_main_grad is not None: | ||
| weight.main_grad = grouped_main_grad[index] | ||
| for attr in ( |
There was a problem hiding this comment.
Same as below:
This non-fused path will operate on the individual expert weights, whereas MCore is attaching distributed training flags to the grouped parent. So we need to pass these flags down to the expert weights.
| "zero_out_wgrad", | ||
| ): | ||
| if hasattr(grouped_weight, attr): | ||
| setattr(weight, attr, getattr(grouped_weight, attr)) |
There was a problem hiding this comment.
why do we need this for loop to setattr?
There was a problem hiding this comment.
This non-fused path will operate on the individual expert weights, whereas MCore is attaching distributed training flags to the grouped parent. So we need to pass these flags down to the expert weights.
| for member in grouped_parameter.quantized_tensors: | ||
| member.requires_grad_(grouped_parameter.requires_grad) | ||
| if all(value is not None for value in high_precision_init_vals): | ||
| _attach_high_precision_init_val( |
There was a problem hiding this comment.
how does it avoid the dequantize exactly?
There was a problem hiding this comment.
This is how TE's initialization contract for high precision works. If the parameter has a method get_high_precision_init_val, then MCore uses that to initialize the optimizer master weights. If its not present, then it dequantizes the model weights.
| if grouped_weight is None: | ||
| raise RuntimeError("Grouped weight was released before backward completed") | ||
|
|
||
| if ctx.fuse_wgrad_accumulation: |
There was a problem hiding this comment.
There needs to be have a numerical test for both fuse_wgrad_accumulation and not fuse_wgrad_accumulation. Compared with discrete weights, with or without the fusion, the wgrad output should be extremely close to discrete.
There was a problem hiding this comment.
Agreed, added a numerical test
| torch.stack(high_precision_init_vals, dim=0), | ||
| ) | ||
| for weight in weights: | ||
| clear = getattr(weight, "clear_high_precision_init_val", None) |
There was a problem hiding this comment.
We need a less hacky implementation.
There was a problem hiding this comment.
We can hide this away by directly using the _ methods I introduced in base.py, but its effectively doing the same thing.
I dont disagree with your general idea that a lot of this is hacky, but as far as I'm aware this is all we can do given the current TE API. Making it less hacky would require a non-trivial redesign and implementation work which I wouldnt want to add to this PR
…ear to use helper functions from base Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
for more information, see https://pre-commit.ci
Signed-off-by: CarlosGomes98 <carlosmiguel.gomes@live.com.pt>
for more information, see https://pre-commit.ci
Description
These changes fix convergence for grouped linear single param.
Issues encountered:
Without the TE fused ops path: causes the experts to get requires_grad=0 and the parent to not receive updates from the individual experts.
With TE fused ops: discards the high precision metadata, so the master weights were starting from dequantized mxfp8 weights, rather than the original high precision weights
Fixes # (issue)
Type of change
Changes
These changes resolve the immediate issue, but cleaner medium- to long-term solutions would require broader changes.
In particular, the bug could likely have been avoided if Transformer Engine supported packing expert weights before quantization. The current flow quantizes each expert separately and then packs the resulting parameters, requiring the packing code to manually preserve and transfer FP8 initialization metadata. This stretches the metadata lifecycle and makes it easy to lose information needed to construct the optimizer’s high-precision master weights. Packing the high-precision weights first and quantizing the grouped parameter once would provide a cleaner and more robust ownership model.
Checklist: