Repository navigation
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/ao/4659
Note: Links to docs will display an error until the docs builds have been completed. ❗ 1 Active SEVsThere are 1 currently active SEVs. If your PR is affected, please view them below: This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
Hi @pculliton! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
| else act_quant_scale.dtype | ||
| ) | ||
|
|
||
| y_dot_scaled = int_scaled_matmul( |
There was a problem hiding this comment.
Why not make this cuda usecase its own tensor subclass? It seems different enough from the rest of the intx tensor?
There was a problem hiding this comment.
Hmm, I think that would make sense, thanks! I'll split it up.
Addresses reviewer feedback from @metascroy to separate the CUDA int_scaled_matmul static activation path from IntxUnpackedToInt8Tensor. - New IntxStaticActivationTensor subclass for static range quantization - Restores IntxUnpackedToInt8Tensor to upstream form (weight-only + dynamic) - Keeps embedding row slicing and aten.select.int as additive improvements - Updates quant_api handler, exports, and safetensors registration
6f1e04b to
3f70ff5
Compare
|
@metascroy pushed 3f70ff5 splitting IntxStaticActivationTensor into its own subclass as suggested - please take a look when you get a chance! |
|
Thanks @pculliton. @metascroy get we please take a look at this once more? |
metascroy
left a comment
There was a problem hiding this comment.
- Please split the
IntxUnpackedToInt8Tensorembedding improvements into their own PR. - The new static tensor should have its own test file.
AI disclosure: Scott requested the two changes above and explicitly asked Devmate to post the actionable findings from its review as inline comments. The AI-generated technical analysis is labeled and quoted in each inline comment.
Validation scope: targeted CPU reproductions used PR modules loaded in memory against local TorchAO dependencies. No full-suite or CUDA execution was performed; the CUDA zero-point example evaluates the branch's arithmetic on CPU. The added static-activation test checks output shapes, but does not check numerical correctness or invoke
torch.compile.
| else: | ||
| w_dequant = weight_tensor.dequantize() | ||
| y = torch.nn.functional.linear(input_tensor, w_dequant, bias) |
There was a problem hiding this comment.
AI-assisted finding (Devmate), posted at Scott's request:
[P1] Apply static activation quantization in the fallback. CPU execution and CUDA execution with grouped weights call floating-point linear on the original input, so
act_quant_scalehas no effect on these paths. For example, with input values of0.25, activation scale1.0, and no bias, activation quantize-dequantize produces zero, but a targeted CPU check returned approximately7.47from this fallback. Quantize-dequantize the activation using the calibrated parameters before the floating-point linear, or explicitly reject unsupported configurations. Please add numerical reference tests for CPU and CUDA grouped-weight execution, not just output-shape assertions.
| w_scales = weight_tensor.scale | ||
| y = (y_dot_scaled * w_scales.flatten()).reshape( | ||
| *input_tensor.shape[:-1], y_dot_scaled.shape[-1] | ||
| ) |
There was a problem hiding this comment.
AI-assisted finding (Devmate), posted at Scott's request:
[P1] Honor asymmetric activation and weight zero points. The config accepts asymmetric quantization, but activation quantization above hardcodes a zero point of zero instead of using
act_quant_zero_point, and this matmul/rescaling omits the weight-zero-point correction. For asymmetric int4 unit weights of shape(2, 32), weight zero points of-8, unit input, and activation scale1.0, evaluating this branch's arithmetic on CPU gives approximately14.93instead of the dequantized reference result32. Implement the affine corrections, or reject/fall back for nonzero zero points. Please cover nonzero activation and weight zero points with CUDA numerical reference tests.
| _adjust_scale_dtype_in_intx_unpacked_tensor( | ||
| new_weight, weight, config.weight_scale_dtype | ||
| ) |
There was a problem hiding this comment.
AI-assisted finding (Devmate), posted at Scott's request:
[P2] Use a scale-dtype adjustment compatible with the new subclass.
new_weightis anIntxStaticActivationTensor, but_adjust_scale_dtype_in_intx_unpacked_tensorexplicitly asserts that its input is anIntxUnpackedToInt8Tensor. These are separate subclasses, so requesting a different scale dtype, such as float32 scales for bfloat16 weights, raises an assertion. Add support for the new type and a test exercisingweight_scale_dtype != weight.dtype.
| if isinstance(self.act_quant_scale, list): | ||
| self.act_quant_scale = torch.tensor(self.act_quant_scale) | ||
| if isinstance(self.act_quant_zero_point, list): | ||
| self.act_quant_zero_point = torch.tensor(self.act_quant_zero_point) | ||
| if isinstance(self.output_quant_scale, list): | ||
| self.output_quant_scale = torch.tensor(self.output_quant_scale) |
There was a problem hiding this comment.
AI-assisted finding (Devmate), posted at Scott's request:
[P2] Restore scalar calibration tensors during JSON deserialization. The new encoder converts a zero-dimensional tensor such as
torch.tensor(0.5)into the JSON number0.5, not a list. These list-only conversions therefore leave the restored scale as a Python float, and the quantization transform fails withAttributeError: 'float' object has no attribute 'detach'. This was reproduced withconfig_from_dict(config_to_dict(config)); scalar scales are also what the added test uses. Handle scalar tensor round-trips as well as lists, including the corresponding conversions inInt8StaticActivationInt8WeightConfig, and test that a restored config can actually quantize a module.
| if padding_idx is not None: | ||
| mask = indices == padding_idx | ||
| weight_sliced_dequant = torch.where( | ||
| mask.unsqueeze(-1), | ||
| torch.zeros_like(weight_sliced_dequant), | ||
| weight_sliced_dequant, | ||
| ) | ||
|
|
||
| return weight_sliced_dequant |
There was a problem hiding this comment.
AI-assisted finding (Devmate), posted at Scott's request:
[P2] Preserve embedding forward semantics in the sliced path.
padding_idxdoes not force the stored padding row to zero during ordinary embedding forward; a loaded or explicitly supplied nonzero row must still be returned. This mask changes that result. The fast path also dropsmax_normandnorm_typeentirely instead of forwarding them as the previous implementation did. A targeted check of the equivalent new static-tensor implementation withmax_norm=1.0returned row norms of approximately5.28, versus1.0from the ordinary embedding reference. Preserve these options in both copies of the sliced implementation, or fall back when necessary. Add tests for a nonzero padding row and max-norm renormalization in the separate embedding PR.
| if getattr(weight_tensor, "act_quant_scale", None) is not None: | ||
| if weight_tensor.act_quant_scale.ndim == 0: | ||
| weight_tensor.act_quant_scale = weight_tensor.act_quant_scale.view( | ||
| (1,) * activation_tensor.ndim | ||
| ) |
There was a problem hiding this comment.
AI-assisted finding (Devmate), posted at Scott's request:
[P2] Normalize the activation scale locally instead of mutating the weight. The first forward replaces a scalar
act_quant_scalewith a tensor whose rank matches that input. Subsequent forwards of another rank no longer enter this branch and reuse the wrong scale shape. With per-tensor static activation quantization, a targeted check succeeded for input shape(1, 32)and then raised an assertion for(1, 1, 32)on the same weight. Keep the stored scalar unchanged, reshape a local scale for each call, and add a successive-forward test covering different input ranks.
…static activation fallback/zero-points/serialization, and add tests
|
Thanks for the review @metascroy! I've updated the PR to address all feedback:
Let me know what you think! |
|
Thanks @pculliton! Overall it looks good to me, but why do you need to extend Int8Tensor in this PR (it seems like an independent feature that could go in its own PR). More generally, does your IntxStaticActivationTensor with x=8 cover the usecases you had for the extension to Int8Tensor? |
Thanks @metascroy! Yep, Good point on the Let me know what you think! |
| if isinstance(o, torch.dtype): | ||
| return {"_type": "torch.dtype", "_data": str(o).split(".")[-1]} | ||
|
|
||
| if isinstance(o, torch.Tensor): |
There was a problem hiding this comment.
why is there a tensor here? this isn't expected in a config
There was a problem hiding this comment.
Thanks! Reverted torchao/core/config.py back to main and removed custom_weight_scale / custom_weight_zero_point (and the JSON list/scalar conversion) from Int8StaticActivationIntxWeightConfig.
For the calibration scales themselves (act_quant_scale, act_quant_zero_point, output_quant_scale), we kept Optional[torch.Tensor] = None on Int8StaticActivationIntxWeightConfig to match Int8StaticActivationInt8WeightConfig (and Float8StaticActivationFloat8WeightConfig) for in-memory per-layer quantization via FqnToConfig, while serialized checkpoints store them directly on IntxStaticActivationTensor in the state_dict.
|
@claude review |
|
Claude finished @vkuzo's task in 1m 51s —— View job Review: SRQ via
|
…t numerics, and slicing
|
Thanks @vkuzo! Pushed an update addressing your comment and the items from the
Please take a look when you get a chance! |
Summary
This PR introduces generic support for Static Range Quantization (SRQ) — static
int8activation quantization,intx(int1..int8) weight quantization, and optional static output quantize-dequantize scaling — via a dedicatedIntxStaticActivationTensorsubclass andInt8StaticActivationIntxWeightConfig.(Note: Per review feedback, the
IntxUnpackedToInt8Tensorembedding and row-select improvements have been split out into a separate PR.)Motivation
Our primary motivation is to support pre-calibrated low-bit (
int2/int4/int8) static range quantization (SRQ) models in TorchAO:IntxStaticActivationTensorsubclass forintxweights (int1..int8) paired with calibrated staticint8activation scales (act_quant_scale,act_quant_zero_point) and optional output scaling (output_quant_scale).state_dictwithweights_only=Trueandsafetensors) storing calibration tensors directly onIntxStaticActivationTensor.Key Changes
1. Core Tensor Subclass (
torchao/quantization/quantize_/workflows/intx/intx_static_activation_tensor.py)IntxStaticActivationTensorto represent unpackedintxweights (torch.int1..torch.int8) with optional pre-calibrated per-tensor or per-channelact_quant_scale,act_quant_zero_point, andoutput_quant_scale.PerAxis(0)) linear layers with per-tensor activation scales (M > 16andK, N % 8 == 0) tosafe_int_mmwith exactint32affine zero-point corrections scaled once infp32, routing smaller/unaligned shapes, grouped (PerGroup) weights, per-channel activation scales, and CPU execution through static activation quantize-dequantize +F.linear..to()(preserving calibration scale dtypes on device-only moves),aten.embedding.default/F.embedding,aten.select.int, andaten.slice.Tensor(including per-channel calibration scale slicing).2. Configuration & Serialization (
torchao/quantization/quant_api.py,torchao/prototype/safetensors/safetensors_utils.py)quant_api.py:Int8StaticActivationIntxWeightConfig(matchingInt8StaticActivationInt8WeightConfigfor in-memory per-layer calibration tensors, pluscustom_scale/custom_zero_pointhandler support for QAT convert)._adjust_scale_dtype_in_intx_unpacked_tensorto supportIntxStaticActivationTensorwhenweight_scale_dtype != weight.dtype.prototype/safetensors/safetensors_utils.py:IntxStaticActivationTensorinALLOWED_CLASSESandALLOWED_TENSORS_SUBCLASSES.3. Unit Tests (
test/quantization/quantize_/workflows/intx/test_intx_static_activation_tensor.py)TestIntxStaticActivationTensorcovering:PerAxis(0)andPerGroup(32), symmetric and asymmetric weights, nonzeroact_quant_zero_point,output_quant_scale,bfloat16/float32scales, and small/large batch sizes (M=2andM=32).safe_int_mmfast-path vs. dequantize fallback selection.weight_scale_dtype != weight.dtype,from_hpcustom weight scale / zero point, andQATConfig(..., step="convert")range-learning conversion..to()device and dtype moves, calibration shape validation, and successive forward passes with varying input ranks (2Dand3D).torch.compile(fullgraph=True)on CPU and CUDA (PerAxis(0)andPerGroup(32),M=2andM=32).config_from_dict(config_to_dict(config))),state_dictserialization (weights_only=True),safetensorsflatten/unflatten,nn.Embedding,select, andslice.Verification
Verified locally on CPU and CUDA: