Quantize activations before EP all gather - #4812
Conversation
There was a problem hiding this comment.
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.
| 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, | ||
| ) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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, ...).
| num_row_partitions = num_cores // num_column_partitions | |
| num_row_partitions = max(1, num_cores // num_column_partitions) |
| if out_dtype is None or jnp.issubdtype(out_dtype, jnp.float8_e4m3fn): | ||
| out_dtype = jnp.bfloat16.dtype |
There was a problem hiding this comment.
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.
| 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 |
| 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) |
There was a problem hiding this comment.
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)| 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) |
There was a problem hiding this comment.
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)| if False and num_iterations > _CostModelConstants.MAX_ITERATIONS: | ||
| break |
There was a problem hiding this comment.
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.
| if False and num_iterations > _CostModelConstants.MAX_ITERATIONS: | |
| break | |
| # TODO: Determine if MAX_ITERATIONS check is still needed or can be removed. |
fac2abc to
1fb67bf
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
9f60c6a to
bbbbaa7
Compare
bbbbaa7 to
5c1f75e
Compare
Quantizes activations to fp8 (via qwix QArray) before the ring-of-experts EP all-gather and the SparseCore ragged-sort/gather kernel, so the collective and gather move fp8 instead of bf16, gated behind a new quantize_before_ep_all_gather flag (default False, requires use_ring_of_experts + qwix quantization + use_gmm_v2). Layers this alongside (not replacing) the existing static/fixed-calibration LHS-scaling mechanism in the gmm v2 kernel.
5c1f75e to
35cf90f
Compare
Quantize activations before EP all-gather (ring-of-experts)
This PR quantizes EP all gather collective and activation sorting when qwix quantization is enabled. This is free because gmm is done in fp8.
Result
xprof (tbd)
Testing strategy
Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.