Skip to content

Quantize activations before EP all gather - #4812

Open
Shuwen-Fang wants to merge 5 commits into
mainfrom
quantize_sort_2
Open

Quantize activations before EP all gather#4812
Shuwen-Fang wants to merge 5 commits into
mainfrom
quantize_sort_2

Conversation

@Shuwen-Fang

Copy link
Copy Markdown
Collaborator

PiperOrigin-RevId: 959862142

Description

Start with a short description of what the PR does and how this is a change from
the past.

The rest of the description includes relevant details and context, examples:

  • why is this change being made,
  • the problem being solved and any relevant context,
  • why this is a good solution,
  • some information about the specific implementation,
  • shortcomings of the solution and possible future improvements.

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

Please describe how you tested this change, and include any instructions and/or
commands to reproduce.

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for Qwix quantization (pre-quantized activations) across GMM and ragged sort/gather kernels, including integration in the MoE layer and a new unit test. While these changes enable quantized execution paths, several critical issues must be addressed: a runtime TypeError in the new unit test due to an unsupported lhs_scale argument in gmm_v2, a potential ZeroDivisionError in ragged_gather_reduce_v2.py when num_cores is 2, and an AttributeError from accessing jnp.bfloat16.dtype. Additionally, shape checks are required in ragged_sort.py before gathering scales to prevent runtime failures on scalar or per-channel scales, private imports in ops.py should be replaced with standard JAX broadcasting, and dead code in ragged_gather_reduce_v2.py should be cleaned up.

Comment on lines +1104 to +1112
actual_prequantized = gmm_backend.gmm_v2(
lhs_q,
rhs_q,
group_sizes,
rhs_scale=rhs_scale,
lhs_scale=lhs_scale,
group_offset=group_offset_arr,
maybe_quantize_lhs=False,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The gmm_v2 function does not accept lhs_scale as a parameter. Passing lhs_scale=lhs_scale here will raise a TypeError at runtime, causing the unit test to fail. Instead, the output of gmm_v2 should be scaled by lhs_scale manually.

Suggested change
actual_prequantized = gmm_backend.gmm_v2(
lhs_q,
rhs_q,
group_sizes,
rhs_scale=rhs_scale,
lhs_scale=lhs_scale,
group_offset=group_offset_arr,
maybe_quantize_lhs=False,
)
actual_prequantized = gmm_backend.gmm_v2(
lhs_q,
rhs_q,
group_sizes,
rhs_scale=rhs_scale,
group_offset=group_offset_arr,
maybe_quantize_lhs=False,
)
actual_prequantized *= lhs_scale.astype(actual_prequantized.dtype)

num_cores = sc_info.num_cores * sc_info.num_subcores

num_column_partitions = _calculate_num_column_partitions(hidden_size, input_size, num_cores, num_lanes, num_simd_lanes)
num_row_partitions = num_cores // num_column_partitions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

When num_cores == 2, num_column_partitions can grow larger than num_cores (e.g., to 4 or 8), which causes num_row_partitions = num_cores // num_column_partitions to evaluate to 0. A value of 0 for num_row_partitions leads to a ZeroDivisionError in _preprocess and main_kernel (e.g., valid_rows_mask.shape[0] // num_row_partitions). Please clamp num_row_partitions to at least 1 using max(1, ...).

Suggested change
num_row_partitions = num_cores // num_column_partitions
num_row_partitions = max(1, num_cores // num_column_partitions)

Comment on lines +1151 to +1152
if out_dtype is None or jnp.issubdtype(out_dtype, jnp.float8_e4m3fn):
out_dtype = jnp.bfloat16.dtype

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In standard JAX/NumPy, jnp.bfloat16 is a type object and does not have a .dtype attribute. Accessing jnp.bfloat16.dtype will raise an AttributeError at runtime. Please use jnp.bfloat16 directly.

Suggested change
if out_dtype is None or jnp.issubdtype(out_dtype, jnp.float8_e4m3fn):
out_dtype = jnp.bfloat16.dtype
if out_dtype is None or jnp.issubdtype(out_dtype, jnp.float8_e4m3fn):
out_dtype = jnp.bfloat16

Comment on lines +107 to +126
if isinstance(hidden_states_local, qpl.QArray):
x_qval = ragged_gather(
hidden_states_local.qvalue,
token_indices_sorted,
shard_output_start[None],
shard_output_end[None],
enforce_fallback=enforce_gather_fallback,
flops_override=gather_flops_override,
bytes_accessed_override=gather_bytes_accessed_override,
)
x_scale = ragged_gather(
hidden_states_local.scale,
token_indices_sorted,
shard_output_start[None],
shard_output_end[None],
enforce_fallback=enforce_gather_fallback,
flops_override=gather_flops_override,
bytes_accessed_override=gather_bytes_accessed_override,
)
x = qpl.QArray(qvalue=x_qval, scale=x_scale)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If hidden_states_local is a QArray with a per-tensor (scalar) or per-channel scale, calling ragged_gather on hidden_states_local.scale will fail at runtime because the scale dimensions do not match the token dimension. Please add a check to only gather the scale if its first dimension matches the token dimension, similar to the logic used in moe.py.

      if isinstance(hidden_states_local, qpl.QArray):
        x_qval = ragged_gather(
            hidden_states_local.qvalue,
            token_indices_sorted,
            shard_output_start[None],
            shard_output_end[None],
            enforce_fallback=enforce_gather_fallback,
            flops_override=gather_flops_override,
            bytes_accessed_override=gather_bytes_accessed_override,
        )
        if hidden_states_local.scale.shape[0] == hidden_states_local.qvalue.shape[0]:
          x_scale = ragged_gather(
              hidden_states_local.scale,
              token_indices_sorted,
              shard_output_start[None],
              shard_output_end[None],
              enforce_fallback=enforce_gather_fallback,
              flops_override=gather_flops_override,
              bytes_accessed_override=gather_bytes_accessed_override,
          )
        else:
          x_scale = hidden_states_local.scale
        x = qpl.QArray(qvalue=x_qval, scale=x_scale)

Comment on lines +151 to +170
if isinstance(hidden_states_local, qpl.QArray):
x_qval = ragged_gather(
hidden_states_local.qvalue,
sliced_indices,
jnp.int32(0)[None],
gather_end[None],
enforce_fallback=enforce_gather_fallback,
flops_override=gather_flops_override,
bytes_accessed_override=gather_bytes_accessed_override,
)
x_scale = ragged_gather(
hidden_states_local.scale,
sliced_indices,
jnp.int32(0)[None],
gather_end[None],
enforce_fallback=enforce_gather_fallback,
flops_override=gather_flops_override,
bytes_accessed_override=gather_bytes_accessed_override,
)
x = qpl.QArray(qvalue=x_qval, scale=x_scale)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If hidden_states_local is a QArray with a per-tensor (scalar) or per-channel scale, calling ragged_gather on hidden_states_local.scale will fail at runtime because the scale dimensions do not match the token dimension. Please add a check to only gather the scale if its first dimension matches the token dimension, similar to the logic used in moe.py.

      if isinstance(hidden_states_local, qpl.QArray):
        x_qval = ragged_gather(
            hidden_states_local.qvalue,
            sliced_indices,
            jnp.int32(0)[None],
            gather_end[None],
            enforce_fallback=enforce_gather_fallback,
            flops_override=gather_flops_override,
            bytes_accessed_override=gather_bytes_accessed_override,
        )
        if hidden_states_local.scale.shape[0] == hidden_states_local.qvalue.shape[0]:
          x_scale = ragged_gather(
              hidden_states_local.scale,
              sliced_indices,
              jnp.int32(0)[None],
              gather_end[None],
              enforce_fallback=enforce_gather_fallback,
              flops_override=gather_flops_override,
              bytes_accessed_override=gather_bytes_accessed_override,
          )
        else:
          x_scale = hidden_states_local.scale
        x = qpl.QArray(qvalue=x_qval, scale=x_scale)

Comment on lines +136 to +137
if lhs_scale is not None:
out = call_with_generic_broadcast(jnp.multiply, out, lhs_scale.astype(out.dtype))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Importing from a private module qwix._src.core.qarray is a maintainability risk as these internal APIs are unstable and can change without notice. Since both out and lhs_scale are standard JAX arrays, standard JAX/NumPy broadcasting (e.g., out * lhs_scale) is fully supported and should be preferred.

Suggested change
if lhs_scale is not None:
out = call_with_generic_broadcast(jnp.multiply, out, lhs_scale.astype(out.dtype))
if lhs_scale is not None:
out = out * lhs_scale.astype(out.dtype)

Comment on lines 224 to 225
if False and num_iterations > _CostModelConstants.MAX_ITERATIONS:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Disabling the MAX_ITERATIONS check with if False and ... introduces dead code and is a code smell. If the check is no longer desired, please remove or comment it out cleanly.

Suggested change
if False and num_iterations > _CostModelConstants.MAX_ITERATIONS:
break
# TODO: Determine if MAX_ITERATIONS check is still needed or can be removed.

Reconciles the ahead-of-time activation quantization (quantize before EP
all-gather / ragged-sort so both move fp8 instead of bf16) with main's
independent history since this branch's stale base:

- ops.py / pallas_mosaic_tpu_v2_gmm_kernel.py: supersedes PR #4735's
  static-LHS-scaling mechanism (_fwd_prepare_lhs_scale/LhsRef), which
  assumes lhs always arrives unquantized and has no QArray handling --
  incompatible with activations arriving pre-quantized. This branch's
  ahead-of-time quantization already covers #4735's target case (qwix's
  own qpl.quantize() natively supports "fixed" calibration), so nothing
  is lost.
- moe.py: re-applied permute()/unpermute()/roe_ag_and_route/extract_vma
  QArray handling and the use_single_sparsecore removal on top of main's
  current moe.py (which independently gained DSV4 aux-loss-free routing
  and unrelated forward-pass bug fixes since this branch's base).
- ragged_gather_reduce_v2.py: also removes disabled dead code
  (`if False and ...`) and a silent broad-except around qwix.dequantize()
  in the SparseCore fallback path.
- Deliberately did not re-apply: stale vllm_batched_rpa/get_logical_axis_rules
  reversions (main already has this content; branch was just behind), or
  the sparse_matmul dispatch restructuring + two quantization-guard
  weakenings (kept main's existing self.config.quantization and ... guards
  intact).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Shuwen-Fang Shuwen-Fang changed the title Merge pull request #4732 from AI-Hypercomputer:fix-qwen-gdn-import Quantize activations before EP all gather Aug 11, 2026
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Shuwen-Fang and others added 2 commits August 11, 2026 00:25
Follow-up to the previous rebase commit -- audited every hunk against
"is this strictly required for quantize-before-EP-all-gather" and reverted
what wasn't:

- moe.py, ragged_sort.py: restore use_single_sparsecore (a SparseCore
  core-count knob for the ragged kernels, unrelated to activation dtype)
  that had been dropped as part of the original PR's own cleanup. main
  already carries it unmodified; only the QArray dual-dispatch is new.
- ragged_gather_reduce_v2.py: revert to main verbatim. The SparseCore
  capacity/tiling fixes (num_cores==2 handling, assert->fallback, etc.)
  aren't quantization-specific -- they'd matter for bf16 runs under the
  same hardware conditions. The QArray-dequant fallback in
  _fallback_implementation is also dead code: checked every call site of
  ragged_gather_reduce and none ever pass it a QArray (only gradients and
  GMM outputs, never quantized activations flow through the reduce path).
- tests/unit/pallas_mosaic_tpu_v2_kernel_test.py: revert to main verbatim.
  test_gmm_prequantized_activation exercises gmm_v2's lhs_scale parameter,
  which belongs to PR #4735's superseded static-scale mechanism and
  doesn't exist in this branch's kernel signature.
- ops.py / pallas_mosaic_tpu_v2_gmm_kernel.py: confirmed already minimal,
  no #4735 mechanism present, only the lhs_q_dtype detection / inner_kernel
  bypass / out_dtype bf16 floor needed for pre-quantized lhs to work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback: don't delete a colleague's in-kernel quantization logic just
because it's currently unreachable for our specific call path -- restore
it as an additional case rather than replacing it, and revisit any actual
overlap in a separate PR/session.

_fwd_run_tokamax_v2 (and make_gmm_configs / inner_kernel in the kernel)
now handle three cases:
  1. lhs is a pre-quantized QArray (ours): unwrap to .qvalue, no in-kernel
     scale, real scale reapplied externally after the kernel call.
  2. lhs is raw with a static "fixed"-calibration scale available (#4735):
     _fwd_prepare_lhs_scale supplies lhs_scale, kernel quantizes internally
     using it instead of a dynamic per-block absmax.
  3. lhs is raw with neither: existing dynamic in-kernel quantization,
     unchanged.

Verified: AOT compile check (train_compile_671b_v6e64.sh, deepseek3-671b
fp8, v6e-64) still compiles cleanly with this layered version, same memory
footprint as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Shuwen-Fang and others added 2 commits August 11, 2026 00:49
Replaces the ops.py-level jnp.issubdtype(int) or jnp.issubdtype(e4m3fn)
enumeration with qwix._src.core.numerics.should_quantize(dtype), which
generalizes correctly to any quantized dtype (e5m2, int4, etc.) instead of
only recognizing float8_e4m3fn specifically -- consistent with the
call_with_generic_broadcast private-qwix-import precedent already in this
file.

Did NOT apply the same change to pallas_mosaic_tpu_v2_gmm_kernel.py: that
file's make_gmm_configs/inner_kernel are shared by the backward DLHS/DRHS
gmm_v2 calls, where gradients are quantized to bwd_qtype=float8_e5m2 (per
the fp8_full recipe) -- a different dtype than the forward act_qtype
(float8_e4m3fn). The old, narrower check only recognized e4m3fn as
"already quantized", so e5m2 gradients fell through to the dynamic
in-kernel quantization branch (safe). Generalizing it there too causes the
kernel to treat e5m2 lhs as already-quantized and hit an unsupported
Mosaic cast (float8_e4m3fn -> float8_e5m2) when pairing it with e4m3fn
rhs -- confirmed by reproducing and reverting just those two call sites.
Left as the original enumeration there, verified via a full AOT compile
check (train_compile_671b_v6e64.sh) both ways.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tize block

1. Factor the 3 near-identical ring_ragged_sort call sites in permute()
   (qvalue, scale, non-QArray) into a local _permute_ring_ragged_sort()
   closure over the shared config args (topk_indices_2d, buffer_size, etc).

2. Remove permute()'s own "pre-quantize before ragged gather" block. It was
   redundant for roe_ag_and_route (ring-of-experts): that caller already
   quantizes x into a QArray before its EP all-gather, so by the time
   permute() ran this block, `not isinstance(inputs_2d, qpl.QArray)` was
   always False and it never actually fired. Worse, it was live but unsafe
   for ra2a_and_route (the non-ring-of-experts caller), which passes x in
   raw: this block would quantize it into a QArray that then flows into
   either jax.lax.ragged_all_to_all (a raw XLA primitive that doesn't
   understand a QArray pytree) or local_permute()/a2a_ragged_sort (which,
   unlike ring_ragged_sort, has no QArray dual-dispatch at all). Since the
   feature is specifically "quantize before EP all-gather" for the
   ring-of-experts path, and roe_ag_and_route already owns that correctly,
   removing this block closes the gap rather than leaving latent-broken
   code in the unrelated ra2a path.

Verified via a full AOT compile check (train_compile_671b_v6e64.sh,
deepseek3-671b fp8, v6e-64, ring-of-experts) -- same memory footprint as
before, confirming roe_ag_and_route's own quantization is sufficient on
its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant