test(fp8): add PyTorch reference parity suite and logit checker metrics - #5055
test(fp8): add PyTorch reference parity suite and logit checker metrics#5055snehalv2002 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for FP8 weight-only storage with dynamic on-the-fly dequantization, specifically adding configurations and mappings for the llama3.1-8b-fp8 model. Key changes include adding fallback mechanisms for scale keys during checkpoint conversion, implementing dynamic dequantization of restored parameters when loading checkpoints, and updating linear layers to support kernel_scale parameters and FP8 weight types. The review feedback focuses on optimizing checkpoint loading by caching keys as a set, replacing dict type checks with collections.abc.Mapping to support FrozenDict, eliminating code duplication by using the centralized is_fp8_dtype helper, and ensuring that bias is initialized in the activation compute dtype when weight_dtype is FP8 to prevent precision loss.
| if self.use_bias: | ||
| bias_axes = self.kernel_axes[-len(self.out_features_shape) :] | ||
| bias_shape = kernel_shape[-len(self.out_features_shape) :] | ||
| try: | ||
| bias_val = default_bias_init(rngs.params(), bias_shape, self.weight_dtype) | ||
| except (TypeError, ValueError): | ||
| bias_val = default_bias_init(rngs.params(), bias_shape, self.dtype).astype(self.weight_dtype) | ||
| self.bias = nnx.Param( | ||
| default_bias_init(rngs.params(), bias_shape, self.weight_dtype), | ||
| bias_val, | ||
| sharding=bias_axes, | ||
| ) |
There was a problem hiding this comment.
When weight_dtype is an FP8 type, bias should be initialized and stored in the compute/activation dtype (e.g., bfloat16) rather than FP8, to avoid precision loss and compatibility issues.
| if self.use_bias: | |
| bias_axes = self.kernel_axes[-len(self.out_features_shape) :] | |
| bias_shape = kernel_shape[-len(self.out_features_shape) :] | |
| try: | |
| bias_val = default_bias_init(rngs.params(), bias_shape, self.weight_dtype) | |
| except (TypeError, ValueError): | |
| bias_val = default_bias_init(rngs.params(), bias_shape, self.dtype).astype(self.weight_dtype) | |
| self.bias = nnx.Param( | |
| default_bias_init(rngs.params(), bias_shape, self.weight_dtype), | |
| bias_val, | |
| sharding=bias_axes, | |
| ) | |
| if self.use_bias: | |
| bias_axes = self.kernel_axes[-len(self.out_features_shape) :] | |
| bias_shape = kernel_shape[-len(self.out_features_shape) :] | |
| bias_dtype = self.dtype if is_fp8_dtype(self.weight_dtype) else self.weight_dtype | |
| try: | |
| bias_val = default_bias_init(rngs.params(), bias_shape, bias_dtype) | |
| except (TypeError, ValueError): | |
| bias_val = default_bias_init(rngs.params(), bias_shape, self.dtype).astype(bias_dtype) | |
| self.bias = nnx.Param( | |
| bias_val, | |
| sharding=bias_axes, | |
| ) |
| import jax | ||
| import jax.numpy as jnp |
There was a problem hiding this comment.
| if not isinstance(want_bare, dict) or not isinstance(meta_tree, dict): | ||
| return meta_tree | ||
|
|
||
| want_keys = set(want_bare.keys()) | ||
| if want_keys and want_keys.issubset(meta_tree.keys()): | ||
| return meta_tree | ||
|
|
||
| for wrapper in ("params", "model_params", "model", "items"): | ||
| if wrapper in meta_tree and isinstance(meta_tree[wrapper], dict): | ||
| sub = meta_tree[wrapper] | ||
| if want_keys and want_keys.issubset(sub.keys()): | ||
| return sub | ||
| if wrapper == "params" and "params" in sub and isinstance(sub["params"], dict): | ||
| if want_keys and want_keys.issubset(sub["params"].keys()): | ||
| return sub["params"] |
There was a problem hiding this comment.
Use Mapping instead of dict to support Flax's FrozenDict and other dictionary-like containers.
| if not isinstance(want_bare, dict) or not isinstance(meta_tree, dict): | |
| return meta_tree | |
| want_keys = set(want_bare.keys()) | |
| if want_keys and want_keys.issubset(meta_tree.keys()): | |
| return meta_tree | |
| for wrapper in ("params", "model_params", "model", "items"): | |
| if wrapper in meta_tree and isinstance(meta_tree[wrapper], dict): | |
| sub = meta_tree[wrapper] | |
| if want_keys and want_keys.issubset(sub.keys()): | |
| return sub | |
| if wrapper == "params" and "params" in sub and isinstance(sub["params"], dict): | |
| if want_keys and want_keys.issubset(sub["params"].keys()): | |
| return sub["params"] | |
| if not isinstance(want_bare, Mapping) or not isinstance(meta_tree, Mapping): | |
| return meta_tree | |
| want_keys = set(want_bare.keys()) | |
| if want_keys and want_keys.issubset(meta_tree.keys()): | |
| return meta_tree | |
| for wrapper in ("params", "model_params", "model", "items"): | |
| if wrapper in meta_tree and isinstance(meta_tree[wrapper], Mapping): | |
| sub = meta_tree[wrapper] | |
| if want_keys and want_keys.issubset(sub.keys()): | |
| return sub | |
| if wrapper == "params" and "params" in sub and isinstance(sub["params"], Mapping): | |
| if want_keys and want_keys.issubset(sub["params"].keys()): | |
| return sub["params"] |
| if not isinstance(want_node, dict) or not isinstance(meta_node, dict): | ||
| return want_node |
There was a problem hiding this comment.
| augmented[k] = _augment_target_with_scales( | ||
| v, | ||
| meta_node.get(k) if isinstance(meta_node, dict) else None, | ||
| ) |
There was a problem hiding this comment.
Use Mapping instead of dict to support Flax's FrozenDict and other dictionary-like containers.
| augmented[k] = _augment_target_with_scales( | |
| v, | |
| meta_node.get(k) if isinstance(meta_node, dict) else None, | |
| ) | |
| augmented[k] = _augment_target_with_scales( | |
| v, | |
| meta_node.get(k) if isinstance(meta_node, Mapping) else None, | |
| ) |
| import flax.linen as nn | ||
|
|
||
| from maxtext.common.common_types import DecoderBlockType, ShardMode, DType, Array, Config | ||
| from maxtext.common.common_types import DecoderBlockType, ShardMode, DType, Array, Config, Shape |
There was a problem hiding this comment.
Instead of duplicating the FP8 check logic locally, import is_fp8_dtype from maxtext.common.common_types to maintain consistency and reduce code duplication.
| from maxtext.common.common_types import DecoderBlockType, ShardMode, DType, Array, Config, Shape | |
| from maxtext.common.common_types import DecoderBlockType, ShardMode, DType, Array, Config, Shape, is_fp8_dtype |
| if has_scale is None: | ||
| should_have_scale = ( | ||
| _is_fp8_dtype(self.weight_dtype) | ||
| or (kernel_scale_init is not None) | ||
| or (scale_shape is not None) | ||
| ) |
There was a problem hiding this comment.
Use the imported is_fp8_dtype instead of the removed local helper.
| if has_scale is None: | |
| should_have_scale = ( | |
| _is_fp8_dtype(self.weight_dtype) | |
| or (kernel_scale_init is not None) | |
| or (scale_shape is not None) | |
| ) | |
| if has_scale is None: | |
| should_have_scale = ( | |
| is_fp8_dtype(self.weight_dtype) | |
| or (kernel_scale_init is not None) | |
| or (scale_shape is not None) | |
| ) |
| if not _is_fp8_dtype(kernel.dtype) and kernel_scale is None: | ||
| kernel = jnp.asarray(kernel, self.dtype) |
There was a problem hiding this comment.
| if has_scale is None: | ||
| should_have_scale = ( | ||
| _is_fp8_dtype(self.weight_dtype) | ||
| or (kernel_scale_init is not None) | ||
| or (scale_shape is not None) | ||
| ) | ||
| else: | ||
| should_have_scale = has_scale |
There was a problem hiding this comment.
Use the imported is_fp8_dtype instead of the removed local helper.
| if has_scale is None: | |
| should_have_scale = ( | |
| _is_fp8_dtype(self.weight_dtype) | |
| or (kernel_scale_init is not None) | |
| or (scale_shape is not None) | |
| ) | |
| else: | |
| should_have_scale = has_scale | |
| if has_scale is None: | |
| should_have_scale = ( | |
| is_fp8_dtype(self.weight_dtype) | |
| or (kernel_scale_init is not None) | |
| or (scale_shape is not None) | |
| ) | |
| else: | |
| should_have_scale = has_scale |
| if _is_fp8_dtype(kernel.dtype) or kernel_scale is not None: | ||
| kernel = dequantize_weight(kernel, kernel_scale, compute_dtype=self.dtype) | ||
| else: | ||
| kernel = jnp.asarray(kernel, self.dtype) |
There was a problem hiding this comment.
Use the imported is_fp8_dtype instead of the removed local helper.
| if _is_fp8_dtype(kernel.dtype) or kernel_scale is not None: | |
| kernel = dequantize_weight(kernel, kernel_scale, compute_dtype=self.dtype) | |
| else: | |
| kernel = jnp.asarray(kernel, self.dtype) | |
| if is_fp8_dtype(kernel.dtype) or kernel_scale is not None: | |
| kernel = dequantize_weight(kernel, kernel_scale, compute_dtype=self.dtype) | |
| else: | |
| kernel = jnp.asarray(kernel, self.dtype) |
7bdd869 to
8007b8f
Compare
b4c07de to
947fa23
Compare
8007b8f to
0700124
Compare
947fa23 to
a69394f
Compare
0700124 to
a9eaede
Compare
a69394f to
7fb243d
Compare
a9eaede to
7230572
Compare
7fb243d to
6e3d31b
Compare
Description
Adds an end-to-end PyTorch reference equivalence test suite and enhances
tests/utils/forward_pass_logit_checker.pywith numerical error and KL divergence metrics for FP8 validation.Motivation & Context
To guarantee numerical correctness and bit-level compatibility between MaxText's dynamic dequantization and reference PyTorch implementations, this PR adds granular module-level unit tests and side-by-side logits comparison tools.
Key Changes
tests/unit/llama_fp8_vs_reference_test.py):DenseGeneralscalar, per-channel, and block-wise scaling parity.MlpBlock(SwiGLU) FP8 parity.Attention(RoPE + GQA) FP8 parity.LlamaDecoderLayerunscanned and scanned parity.NNXDecoderpipeline parity.tests/utils/forward_pass_logit_checker.py):atol), relative difference (rtol), top-k rank agreement, and KL divergence (--run_hf_model=True.Part 5 of 5 in the FP8 Weight-Only Dynamic Dequantization series (depends on #5053, #5052, #5051, #5054).
If the change fixes a bug or a Github issue, please include a link, e.g.,:
FIXES: b/123456
FIXES: #123456
You can also provide a comma-separated list. If you don't want to close a bug but
simply to reference it, use BUGS, e.g.:
BUGS: b/123456
Notice 1: Once all tests pass, the "pull ready" label will automatically be assigned.
This label is used for administrative purposes. Please do not add it manually.
Notice 2: For external contributions, our settings currently require an approval from a MaxText maintainer to trigger CI tests.
Tests
Ran the complete reference parity test suite:
Result:
5 passed, 0 failures.Ran side-by-side forward pass logit verification against Hugging Face reference model (
neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8):Result:
4.7988e-03(<0.05limit).Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.