feat(kda): integrate KDA attention with tokamax backend and CP support - #5128
feat(kda): integrate KDA attention with tokamax backend and CP support#5128chiaotung97 wants to merge 14 commits into
Conversation
- Add KimiDeltaAttention layer (attention_kda.py) with QKV projections, ShortConvolution, gate/beta/output-gate projections - Add KDA kernel dispatch (kernels/kda/__init__.py) delegating to tokamax - Add tokamax backend adapter (kernels/kda/tokamax.py) with layout translation - Add CP utilities (cp_utils.py) for halo exchange and AG-CP support - Add KdaAttention config class (types.py) with kda_backend field - Add base.yml config entry for kda_backend - Add comprehensive unit tests (kda_attention_test.py) - Add KDA+CP support design doc (docs/design/kda_cp_support.md)
P0 fixes: - Replace all AG-CP/All-Gather CP references with CP (23 occurrences) - Remove tops/pallas-kernel references from base.yml and types.py - Add comment explaining tokamax's pallas_tpu implementation name P1 fixes: - Remove unused kda_backend parameter from chunk_kda and config - Update design doc scope to reflect one-time KDA+CP integration P2 fixes: - Replace assert statements with raise (NotImplementedError, ValueError, ImportError) - Fix misleading test name (test_kda_cp_no_load_balance_ok -> test_kda_no_cp_without_load_balance_ok)
- Fix test method name: test_kda_ag_cp_equivalence -> test_kda_cp_equivalence - Add warning when kda_lower_bound is set but safe_gate=False - Add ge=0 constraint on linear_conv_kernel_dim in types.py - Add field_validator for kda_lower_bound to reject NaN/Inf
- Apply pyink auto-formatting (line-length=122, indent=2) - Fix design doc: Assert -> raise ImportError for CPContext check
…tention Renames stale parameters to the finalized tokamax API (a_log, delta_time_bias, use_qk_l2norm, max_num_segments, context_parallel_metadata), updates config docs to the sigmoid lower-bound gate semantics, and adds license headers.
- Thread cfg.context_sharding through the conv halo exchange, T-axis pspec injection (now an unconditional overwrite) and ContextParallelMetadata, fixing latent breakage under expert-as-context sharding. - Fail fast with a config-level message when packed sequences are used without a positive max_segments_per_seq. - Config validators: use_kda_safe_gate=True requires kda_lower_bound in [-5, 0); reject use_kda_lora=True (unimplemented no-op). - Fix linear_conv_kernel_dim docs (convolution applies to Q/K/V, not only keys); refresh design doc file/test tables. - New tests: CP=2/4 parametrized forward equivalence, CP gradient equivalence, full-layer CP with the internal dummy-segment path, parametrized ShortConv cross-rank segment boundaries, l2-norm unit-norm assertion, within-row packed-segment isolation, and config guard tests. - pyink the e2e smoke script.
- e2e smoke: replace the permutation task with a history-dependent delayed-copy task (i.i.d. tokens, t[i] = t[i-delay], unpredictable positions masked out of the loss) so the smoke can no longer be solved without the recurrent KDA state; validate args via parser.error instead of assert - tests: add full-layer CP with real packed segments (a segment spanning the rank boundary + a boundary exactly at the split; forward/input/weight grads vs non-CP), full-layer Mosaic vs tokamax XLA reference parity, and oversized CP-halo rejection; suite grows from 30 to 43 items, all passing on 4xTPU v6e - tests: refine the tpu_only marker from module-level to per-test so pure config/pure-op/non-CP tests run in regular CPU CI; drop the unconditional prints in _assert_close (diagnostics now only in the assertion failure message); add _assert_rel_l2_close for accumulated weight-gradient comparisons - cp_utils: raise a clear ValueError when halo_size > T_local under CP (multi-rank receptive field is not implemented) - docs/design/kda_cp_support.md: sync snippets with the implementation (unconditional T-axis overwrite, cfg.context_sharding / expert-as- context), add new test entries, mdformat-clean - tokamax adapter: document the deliberate lazy import; the pin bump must wait for the first public tokamax release containing KDA (openxla/tokamax#1103 is still open)
- types.py: drop superfluous parens after not (C0325, the finding that failed the Code Quality Check pylint step) - tokamax adapter: validate initial_state/output_final_state before the lazy tokamax import so the guards fire on installs without tokamax - tests: kernel input-guard tests (initial_state / output_final_state on both chunk_kda and the tokamax adapter), ShortConvolution feature-mismatch and kernel_size=1 cases, and a single-rank shard_map halo-exchange case — all CPU-runnable; plus tpu_only coverage for the no-conv forward path, the safe-gate warning, and the CP metadata-missing refusal
- The delayed-copy task with delay=4 could be solved by the 4-tap causal ShortConvolution alone (its window [j-3, j] contains the target t[j-3]), so the loss drop was not evidence of recurrent-state carry. Default the delay to 8, outside the receptive field, and enforce delay > linear_conv_kernel_dim at startup; bump the default step count to 600 for the harder task. Rerun on 4xTPU v6e: 5.43 -> 0.42, PASS. - tokamax adapter TODO: the KDA API has landed on openxla/tokamax main (PR AI-Hypercomputer#1103 was left open but its content is on main, signature and ContextParallelMetadata both compatible); no public release contains it yet, so the pin bump still waits on the first release.
The reviewer's checklist item asks for new documentation pages to be added to the relevant toctree. Move docs/design/kda_cp_support.md to docs/reference/kda_cp_support.md (next to the peer MTP CP doc) and register it in the Reference section: a navigation card plus a hidden-toctree entry, same as the existing reference pages. Refresh the doc's Files Changed stats and its self-reference to the new path.
There was a problem hiding this comment.
Code Review
This pull request integrates Kimi Delta Attention (KDA) into MaxText with tokamax backend and context parallelism (CP) support, introducing the KimiDeltaAttention layer, ShortConvolution, and CP-aware causal convolution boundary handling. The review feedback identifies several critical issues: a subscripting error on nnx.Param in ShortConvolution, a JAX compilation error when passing None to shard_map under CP without user-provided segment IDs, and an incorrect parameter count calculation in the smoke test script due to Flax NNX Variable wrapping. Addressing these issues by accessing .value on NNX parameters/variables and synthesizing dummy segment IDs outside of shard_map will ensure correctness and successful compilation.
| safe_gate = cfg.use_kda_safe_gate | ||
| lower_bound = cfg.kda_lower_bound if safe_gate else None | ||
| if not safe_gate and cfg.kda_lower_bound != 0.0: | ||
| warnings.warn( | ||
| f"kda_lower_bound={cfg.kda_lower_bound} is ignored because use_kda_safe_gate=False. " | ||
| "Set use_kda_safe_gate=True to enable lower_bound clamping.", | ||
| stacklevel=2, | ||
| ) | ||
| n_max = cfg.max_segments_per_seq if cfg.max_segments_per_seq > 0 else None |
There was a problem hiding this comment.
Under context parallelism (cp_size > 1) without user-provided decoder_segment_ids, passing None to shard_map while specifying seg_pspec in in_specs will cause a JAX compilation error because shard_map expects all sharded inputs to be JAX arrays.
To prevent this, synthesize the dummy decoder_segment_ids (all-ones) outside of shard_map and set n_max = 1 here. This ensures a valid JAX array is sharded and passed to shard_map normally.
| safe_gate = cfg.use_kda_safe_gate | |
| lower_bound = cfg.kda_lower_bound if safe_gate else None | |
| if not safe_gate and cfg.kda_lower_bound != 0.0: | |
| warnings.warn( | |
| f"kda_lower_bound={cfg.kda_lower_bound} is ignored because use_kda_safe_gate=False. " | |
| "Set use_kda_safe_gate=True to enable lower_bound clamping.", | |
| stacklevel=2, | |
| ) | |
| n_max = cfg.max_segments_per_seq if cfg.max_segments_per_seq > 0 else None | |
| safe_gate = cfg.use_kda_safe_gate | |
| lower_bound = cfg.kda_lower_bound if safe_gate else None | |
| if not safe_gate and cfg.kda_lower_bound != 0.0: | |
| warnings.warn( | |
| f"kda_lower_bound={cfg.kda_lower_bound} is ignored because use_kda_safe_gate=False. " | |
| "Set use_kda_safe_gate=True to enable lower_bound clamping.", | |
| stacklevel=2, | |
| ) | |
| n_max = cfg.max_segments_per_seq if cfg.max_segments_per_seq > 0 else None | |
| if decoder_segment_ids is None and cp_size > 1: | |
| decoder_segment_ids = jnp.ones((B, T), dtype=jnp.int32) | |
| n_max = 1 |
| def _shard_map_chunk_kda(*args): | ||
| q, k, v, g, beta, a_log, delta_time_bias_2d, *rest = args | ||
| seg = rest[0] if rest else None | ||
| max_num_segments = n_max | ||
|
|
||
| # CP: provide a dummy seg (all-ones) so the kernel has | ||
| # segment_ids to derive cu_seqlens from, even when the user | ||
| # hasn't supplied real segmentation info. | ||
| if seg is None and cp_size > 1: | ||
| seg = jnp.ones(q.shape[:2], dtype=jnp.int32) | ||
| max_num_segments = 1 |
There was a problem hiding this comment.
Since decoder_segment_ids is now synthesized outside of shard_map, we can simplify the internal helper function by removing the None check and the local dummy array creation.
| def _shard_map_chunk_kda(*args): | |
| q, k, v, g, beta, a_log, delta_time_bias_2d, *rest = args | |
| seg = rest[0] if rest else None | |
| max_num_segments = n_max | |
| # CP: provide a dummy seg (all-ones) so the kernel has | |
| # segment_ids to derive cu_seqlens from, even when the user | |
| # hasn't supplied real segmentation info. | |
| if seg is None and cp_size > 1: | |
| seg = jnp.ones(q.shape[:2], dtype=jnp.int32) | |
| max_num_segments = 1 | |
| def _shard_map_chunk_kda(*args): | |
| q, k, v, g, beta, a_log, delta_time_bias_2d, *rest = args | |
| seg = rest[0] if rest else None |
| if has_seg: | ||
| if decoder_segment_ids is not None: | ||
| kda_args = kda_args + (decoder_segment_ids,) | ||
| else: | ||
| # CP without varlen: pass None; the shard_map function | ||
| # synthesises a dummy seg internally. | ||
| kda_args = kda_args + (None,) | ||
| o = _shard_map_chunk_kda(*kda_args) |
There was a problem hiding this comment.
There was a problem hiding this comment.
has_seg is now simply decoder_segment_ids is not None (CP without user segments synthesizes the tensor earlier), so kda_args never appends None.
| rngs = nnx.Rngs(0) | ||
| with mesh: | ||
| model = TinyKdaLM(cfg, mesh, args.num_layers, rngs=rngs) | ||
| n_params = sum(v.size for v in jax.tree.leaves(nnx.state(model)) if isinstance(v, (jax.Array, np.ndarray))) |
There was a problem hiding this comment.
In Flax NNX, nnx.state(model) returns a State dict containing Variable objects (like Param). Checking isinstance(v, (jax.Array, np.ndarray)) on these variables will evaluate to False, resulting in n_params being calculated as 0. Access the underlying array using v.value for any nnx.Variable instead.
| n_params = sum(v.size for v in jax.tree.leaves(nnx.state(model)) if isinstance(v, (jax.Array, np.ndarray))) | |
| n_params = sum(v.value.size for v in jax.tree.leaves(nnx.state(model)) if isinstance(v, nnx.Variable)) |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| print(f"devices: {devices}") | ||
| mesh = jax.sharding.Mesh(np.array(devices), ("x",)) | ||
| rngs = nnx.Rngs(0) | ||
| with mesh: |
There was a problem hiding this comment.
with jax.set_mesh(mesh):
with mesh is deprecated
There was a problem hiding this comment.
Removed entirely rather than migrating to jax.set_mesh: the mesh is passed explicitly to the layer/shard_map.
It forces Manual axis types that clash with the layer's explicit Auto-typed shard maps. Consistent with upstream CP tests, which also use no ambient mesh context.
| @@ -0,0 +1,235 @@ | |||
| # Copyright 2026 Ant Group. All Rights Reserved. | |||
There was a problem hiding this comment.
could you either update this as a test, or move it to src/maxtext/examples/.
There was a problem hiding this comment.
Converted to a test: TestKdaE2eSmoke::test_delayed_copy_loss_collapses (tpu_only, ~1 min on v6e). It keeps the history-dependent delayed-copy task (delay chosen to exceed the conv receptive field so only the KDA recurrent state can solve it) and asserts the loss collapses. The scripts/dev/ script is removed.
| ) | ||
|
|
||
|
|
||
| class KdaAttention(BaseModel): |
There was a problem hiding this comment.
please also add new flags in configs/base.yml
There was a problem hiding this comment.
Done, linear_conv_kernel_dim, use_kda_safe_gate, kda_lower_bound, use_kda_lora are registered in configs/base.yml next to the other attention options, matching the KdaAttention field names/defaults in types.py.
| @@ -0,0 +1,95 @@ | |||
| # Copyright 2026 Ant Group. All Rights Reserved. | |||
There was a problem hiding this comment.
if this util is not for kda, I suggest merging it into attention_kda.py
There was a problem hiding this comment.
Yes, it's KDA-specific, so it is good to merge into attention_kda.py and removed utils/cp_utils.py, Thanks.
NuojCheng
left a comment
There was a problem hiding this comment.
Thank you for contribution! Some general architecture comments before diving deep
Review-driven changes: - scripts/dev/kda_e2e_smoke.py is converted into a proper test (TestKdaE2eSmoke::test_delayed_copy_loss_collapses), tuned to converge in ~1 min on v6e; the standalone script is removed (NuojCheng) - KDA flags registered in configs/base.yml: linear_conv_kernel_dim, use_kda_safe_gate, kda_lower_bound, use_kda_lora (NuojCheng) - halo_exchange_for_conv merged into attention_kda.py — it is KDA-specific; utils/cp_utils.py removed (NuojCheng) - deprecated `with mesh:` context is gone from the tests: the mesh is passed explicitly everywhere and no ambient context is needed (verified empirically; jax.set_mesh cannot be used here — it forces Manual axis types that clash with the layer's explicit Auto-typed shard_maps) - design doc moved to docs/reference/kda_cp_support.md and linked in the Reference toctree/card grid (checklist item) Upstream CI fixes (the tpu-unit / pathways failures): - every kernel-invoking test now carries skipif(not TOKAMAX_AVAILABLE) where TOKAMAX_AVAILABLE means the KDA API is importable — upstream CI installs a released tokamax without the KDA module, so those tests now skip cleanly there (they still run on hosts with KDA-enabled tokamax) - the adapter's lazy import now raises an explanatory ImportError naming the missing tokamax KDA API and how to get it Gemini review: - CP without user segment_ids now synthesizes the all-ones segment tensor outside shard_map (n_max=1), simplifying the shard_map body and the kda_args construction (G2-G4) - the G1 kernel.value suggestion is not applied: on current flax both `.value` and `[...]` access warn, and `[...]` is the form flax itself recommends for array variables; the original code was correct
Thank you for detailed comments. All four addressed in the latest push: |
Full decoder integration so attention_type='kda' trains through the real MaxText stack (mirrors the MLA selection pattern): - common_types: AttentionType.KDA - types.py: attention_type Literal gains 'kda'; cross-validator rejects attention_type='kda' with scan_layers=true (KDA layers are not validated inside a scanned stack yet) - base.yml: attention_type supported list + KDA flag block restored on top of pristine content (an earlier lint run had corrupted the YAML) - nnx_decoders.NNXDecoderLayer: per-layer attention_type override + KDA branch that builds KimiDeltaAttention (no KV cache; recurrent state is carried by the kernel); non-KDA types unaffected - KimiDeltaAttention now always L2-normalizes Q/K: the Delta-Rule recurrence diverges to NaN in bf16 with unbounded q/k (verified on v6e), and QK L2-norm is part of the KDA reference architecture; this is independent of the shared use_qk_norm flag, which belongs to dot-product attention New tests (tests/unit/kda_decoder_integration_test.py): - CPU: config acceptance, scan_layers guard, layer dispatch to KimiDeltaAttention vs regular Attention - TPU: real-decoder training run (delayed-copy task) — loss collapses from 5.28 to well under 1.0 in 400 steps through the actual decoder Verified on 4xTPU v6e: 35 TPU tests + 23 CPU tests pass across both KDA test files.
The patch number reflects what CI can measure, not what the code covers: every KDA kernel-invoking path is exercised by Running the full suite on an actual TPU host (4×TPU v6e, both TPU mode and CPU mode) measures 99% line coverage on the new KDA source files:
The only uncovered lines are the Once tokamax ships the first release containing the KDA API and CI can install it, the kernel paths will be exercised in CI as well and the patch number will reflect them. |
Upstream Code Quality flagged use-dict-literal in kda_decoder_integration_test.py; convert the defaults dict(...) call to a literal (pylint passes with the exact pre-commit hook invocation).
Description
This PR integrates KDA (Kimi Delta Attention, a recurrent linear attention mechanism) into MaxText end to end: the attention layer, the tokamax-backed Pallas TPU kernel, context parallelism (CP), and decoder wiring via
attention_type='kda'. KDA updates its recurrent state with the Delta Rule:The layer follows the Megatron KDA reference and delegates kernel execution to tokamax's Pallas TPU implementation, keeping MaxText free of low-level kernel code.
Key Changes
src/maxtext/layers/attention_kda.pyKimiDeltaAttentionlayer andShortConvolution: QKV/beta/gate/output-gate projections, depthwise causal 1D convolution, SiLU activation, always-on QK L2 normalization, per-head RMSNorm + output gate, gate parametersA_log/dt_bias(matching the Megatron reference); also hostshalo_exchange_for_convfor CPsrc/maxtext/kernels/kda/chunk_kdaentry point + tokamax adapter:[B,T,H,D]↔[H,B,T,D]layout translation, lazy import of the KDA API, explicitmosaicimplementation selection insideshard_mapsrc/maxtext/common/common_types.pyAttentionType.KDAsrc/maxtext/configs/types.pyKdaAttentionconfig (linear_conv_kernel_dim,use_kda_safe_gate,kda_lower_bound, reserveduse_kda_lora) + validators;attention_typeLiteral gains"kda";attention_type='kda'withscan_layers=trueis rejected at config timesrc/maxtext/configs/base.ymlattention_typesupported listsrc/maxtext/layers/nnx_decoders.pyNNXDecoderLayergains a per-layerattention_typeoverride and a KDA branch: buildsKimiDeltaAttention(no KV cache — recurrent state is carried by the kernel); non-KDA types are untouchedtests/unit/kda_attention_test.pytests/unit/kda_decoder_integration_test.pydocs/reference/kda_cp_support.mdUsing KDA
Select it like any other attention variant — this mirrors how
attention_type='mla'is consumed:Notes:
use_qk_normflag which belongs to dot-product attention.context_parallel_load_balanceis rejected with KDA+CP (token order is load-bearing for the recurrent state).NotImplementedError). Train/prefill paths are supported.Context Parallelism Support
cfg.context_sharding(default"context";"expert"works for expert-as-context) and is threaded consistently through the conv halo exchange, the T-axis partition-spec injection, andContextParallelMetadatashard_mapso the kernel can always derive per-rankcu_seqlens/ chain fieldsShortConvolutionpullskernel_size-1left-context tokens from the previous CP rank viappermuteinside a dedicatedshard_map; a clearValueErrorguards thekernel_size-1 > T_localcase (multi-rank receptive fields are not implemented)Tests and Validation
Unit tests — 35 TPU tests + 23 CPU tests, all passing (4×TPU v6e, 2026-09-04):
initial_state/output_final_staterejected with clear errorstpu_onlyis applied per-test, so the pure config/pure-op tests also run in regular CPU CIDecoder integration (
tests/unit/kda_decoder_integration_test.py):scan_layersguard, dispatch ofNNXDecoderLayertoKimiDeltaAttention(and unchanged default for other types)decoder_block=default, 2 layers, delayed-copy task — i.i.d. tokens witht[i] = t[i-delay],delay=5beyond the conv receptive field, so only the recurrent state can solve it; masked loss): loss collapses from 5.28 to < 1.0 in 400 steps through the actual decoderReproducing
On a TPU host (Python ≥ 3.12; verified on 4×TPU v6e):
Dependencies
main(signature-compatible with this PR's adapter, verified), but no public tokamax release contains it yet. MaxText's pin therefore cannot express the dependency; the adapter imports the KDA API lazily, so installs without it are unaffected until KDA is actually used (clearImportErrorwith instructions at that point). Once the first release ships, thetokamax>=pin and derived requirement files will be bumped.Hardware / Shape Constraints (mosaic kernel)
The adapter selects the
"mosaic"Pallas implementation explicitly (no silent fallback). Constraints — surfaced as clearNotImplementedErrors from tokamax at bind time:Known Limitations / Follow-ups
initial_state/output_final_statenot yet supported (explicitNotImplementedError)scan_layersmust befalsewithattention_type='kda'(scanned KDA stacks not validated yet)Checklist
gemini-reviewlabel.