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) |
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.