Skip to content

Commit ef9cc43

Browse files
committed
Rewrite Gemma4 scannable block
1 parent 2aa4254 commit ef9cc43

6 files changed

Lines changed: 463 additions & 28 deletions

File tree

src/maxtext/layers/decoders.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,7 +1426,12 @@ def _apply_gemma4_scanned_blocks(
14261426
if num_full_blocks > 0:
14271427
ScannableBlockToLinen = gemma4.Gemma4ScannableBlockToLinen
14281428
policy = self.get_remat_policy()
1429-
RemattedGemma4Block = self.set_remat_policy([ScannableBlockToLinen], policy)[0]
1429+
# Gemma4ScannableBlock rematerializes its own local (scanned) and global
1430+
# layers when apply_internal_remat=True, so we do NOT wrap it in
1431+
# block-level remat here (that would double-rematerialize and make XLA
1432+
# treat the whole block as one unit). Unrolling the block scan lets XLA
1433+
# free each block's activations across iterations instead of keeping the
1434+
# block live as a unit.
14301435

14311436
kv_cache_scanned = maxtext_utils.prepare_kv_caches_for_scan(
14321437
kv_caches, num_full_blocks, block_pattern_len, stack=True
@@ -1449,7 +1454,7 @@ def _apply_gemma4_scanned_blocks(
14491454

14501455
# For a fully scanned block, apply it inside a nn.scan over the calculated number of full blocks
14511456
y, returned_kv_cache = nn.scan(
1452-
RemattedGemma4Block,
1457+
ScannableBlockToLinen,
14531458
variable_axes={
14541459
"params": cfg.param_scan_axis,
14551460
"cache": 0,
@@ -1460,6 +1465,7 @@ def _apply_gemma4_scanned_blocks(
14601465
split_rngs={"params": True, "dropout": cfg.enable_dropout},
14611466
in_axes=in_axes_tuple,
14621467
length=num_full_blocks,
1468+
unroll=num_full_blocks,
14631469
metadata_params={
14641470
nn.PARTITION_NAME: "layers",
14651471
"abstract_init": False,
@@ -1470,6 +1476,8 @@ def _apply_gemma4_scanned_blocks(
14701476
quant=self.quant,
14711477
model_mode=model_mode,
14721478
num_of_layers=block_pattern_len,
1479+
remat_policy_fn=policy,
1480+
apply_internal_remat=True,
14731481
name="scanned_blocks",
14741482
)(
14751483
y, *broadcast_args

src/maxtext/layers/nnx_decoders.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -646,9 +646,19 @@ def _init_scanned_gemma4(self, decoder_block_classes, rngs, mesh):
646646
attention_pattern_length = len(gemma4.GEMMA4_ATTENTION_PATTERN)
647647
scan_length = config.num_decoder_layers // attention_pattern_length
648648
num_remaining_layers = config.num_decoder_layers % attention_pattern_length
649-
layer_kwargs = {"num_of_layers": attention_pattern_length}
650-
651-
rem_layer_kwargs = {"num_of_layers": num_remaining_layers}
649+
policy = self.get_remat_policy()
650+
# The pure-NNX decoder skips block-level remat (skip_block_remat=True below),
651+
# so the block rematerializes its own local/global layers instead.
652+
layer_kwargs = {
653+
"num_of_layers": attention_pattern_length,
654+
"remat_policy_fn": policy,
655+
"apply_internal_remat": True,
656+
}
657+
rem_layer_kwargs = {
658+
"num_of_layers": num_remaining_layers,
659+
"remat_policy_fn": policy,
660+
"apply_internal_remat": True,
661+
}
652662

653663
RemattedGemma4Block = gemma4.Gemma4ScannableBlock
654664

@@ -870,7 +880,17 @@ def pure_layer_fn(state_in, y_in):
870880

871881
return out
872882

873-
def _apply_layers_sequentially(self, layers, x_in, *args, length: int, kv_caches_stacked=None, **kwargs):
883+
def _apply_layers_sequentially(
884+
self,
885+
layers,
886+
x_in,
887+
*args,
888+
length: int,
889+
kv_caches_stacked=None,
890+
skip_block_remat: bool = False,
891+
unroll: int = 1,
892+
**kwargs,
893+
):
874894
"""Runs the layer stack using nnx.scan.
875895
876896
Args:
@@ -881,6 +901,11 @@ def _apply_layers_sequentially(self, layers, x_in, *args, length: int, kv_caches
881901
kv_caches_stacked: Optional pytree whose leaves have shape [num_layers, ...].
882902
When provided, the i-th slice is passed as `kv_cache=` to layer i and the
883903
updated caches are returned as a third element of the tuple.
904+
skip_block_remat: When True, do not wrap the scanned body in jax.checkpoint.
905+
Used when the scanned module already applies its own (finer-grained,
906+
e.g. per-layer) remat internally, to avoid double rematerialization.
907+
unroll: Number of scan iterations to unroll into straight-line code
908+
(forwarded to jax.lax.scan). unroll >= length fully unrolls the loop.
884909
**kwargs: Keyword args forwarded to the layer (filtered by the layer signature).
885910
886911
Returns:
@@ -963,7 +988,12 @@ def layer_fn(carry, scanned_vars):
963988
return new_carry, (new_current_state, updated_kv)
964989
return new_carry, new_current_state
965990

966-
layer_fn_wrapped = jax.checkpoint(layer_fn, policy=policy, prevent_cse=prevent_cse)
991+
if skip_block_remat:
992+
# The scanned module applies its own remat internally; wrapping the whole
993+
# body again would double-remat and recompute the entire block.
994+
layer_fn_wrapped = layer_fn
995+
else:
996+
layer_fn_wrapped = jax.checkpoint(layer_fn, policy=policy, prevent_cse=prevent_cse)
967997

968998
if use_kv:
969999
# If kv_caches is provided (e.g., from vLLM), we CANNOT use jax.lax.scan
@@ -995,7 +1025,7 @@ def layer_fn(carry, scanned_vars):
9951025
params = maxtext_utils_nnx.nnx_ensure_scan_leading_axis(params, length)
9961026
state = maxtext_utils_nnx.nnx_ensure_scan_leading_axis(state, length)
9971027

998-
final_carry, scanned_state = jax.lax.scan(layer_fn_wrapped, x_in, (params, state))
1028+
final_carry, scanned_state = jax.lax.scan(layer_fn_wrapped, x_in, (params, state), unroll=unroll)
9991029
returned_kv_stacked = None
10001030

10011031
# Ensure metadata rank matches the stacked values
@@ -1938,13 +1968,25 @@ def _apply_gemma4_scanned_blocks(
19381968
attention_pattern_length = len(gemma4.GEMMA4_ATTENTION_PATTERN)
19391969
scan_length = cfg.num_decoder_layers // attention_pattern_length
19401970

1941-
# Apply the main scan over the full blocks
1971+
# Apply the main scan over the full blocks. Gemma4ScannableBlock applies
1972+
# per-layer remat internally (local scan + global layer), so skip the
1973+
# block-level remat here to avoid double rematerialization. Unrolling the
1974+
# block loop (one iteration per repeated block) lets XLA pipeline/free block
1975+
# activations across iterations (memory + overlap knob).
1976+
block_unroll = max(1, scan_length)
19421977
if scan_length > 0:
19431978
grouped_kv_caches = maxtext_utils.prepare_kv_caches_for_scan(
19441979
kv_caches, scan_length, attention_pattern_length, stack=False
19451980
)
19461981
y, self.scanned_blocks, _ = self._apply_layers_sequentially(
1947-
self.scanned_blocks, y, *layer_args, length=scan_length, kv_caches_stacked=grouped_kv_caches, **layer_kwargs
1982+
self.scanned_blocks,
1983+
y,
1984+
*layer_args,
1985+
length=scan_length,
1986+
kv_caches_stacked=grouped_kv_caches,
1987+
skip_block_remat=True,
1988+
unroll=block_unroll,
1989+
**layer_kwargs,
19481990
)
19491991
maxtext_utils.update_kv_caches_after_scan(
19501992
kv_caches, grouped_kv_caches, scan_length, attention_pattern_length, stacked=False

src/maxtext/layers/nnx_scan.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Utilities for constructing stacks of scanned NNX layers."""
15+
"""Utilities for constructing and applying stacks of scanned NNX layers."""
1616

1717
from collections.abc import Callable
18+
from typing import Any
1819

1920
from flax import nnx
2021
import jax
@@ -87,3 +88,50 @@ def update_leaf(leaf):
8788
stacked_params = add_scan_metadata(stacked_params, param_scan_axis)
8889
stacked_rest = add_scan_metadata(stacked_rest, 0)
8990
return nnx.merge(layer_graphdef, stacked_params, stacked_rest)
91+
92+
93+
def apply_scanned_layers(
94+
layers: nnx.Module,
95+
carry: Any,
96+
*,
97+
length: int,
98+
param_scan_axis: int,
99+
apply_fn: Callable[[nnx.Module, Any], Any],
100+
remat: bool = False,
101+
remat_policy: Callable[..., Any] | None = None,
102+
prevent_cse: bool = True,
103+
unroll: int = 1,
104+
) -> Any:
105+
"""Applies stacked NNX layers using ``jax.lax.scan``.
106+
107+
This helper owns the generic NNX state and scan-axis mechanics. ``apply_fn``
108+
defines the model-specific module invocation and must return the next carry.
109+
``remat`` is separate from ``remat_policy`` because ``None`` is JAX's full
110+
rematerialization policy, not an indication that rematerialization is off.
111+
112+
Externally managed per-layer state, such as KV caches, is not supported by
113+
this scan path.
114+
"""
115+
if length <= 0:
116+
return carry
117+
118+
layer_graphdef, params, state = nnx.split(layers, nnx.Param, ...)
119+
if param_scan_axis != 0:
120+
params = jax.tree.map(lambda x: jnp.moveaxis(x, param_scan_axis, 0), params)
121+
122+
def scan_body(current_carry, scanned_state):
123+
current_params, current_state = scanned_state
124+
current_layer = nnx.merge(layer_graphdef, current_params, current_state)
125+
next_carry = apply_fn(current_layer, current_carry)
126+
return next_carry, nnx.state(current_layer)
127+
128+
scan_fn = jax.checkpoint(scan_body, policy=remat_policy, prevent_cse=prevent_cse) if remat else scan_body
129+
final_carry, scanned_state = jax.lax.scan(scan_fn, carry, (params, state), unroll=unroll)
130+
131+
if param_scan_axis != 0:
132+
scanned_params, scanned_other = scanned_state.split(nnx.Param, ...)
133+
scanned_params = jax.tree.map(lambda x: jnp.moveaxis(x, 0, param_scan_axis), scanned_params)
134+
scanned_state = nnx.State.merge(scanned_params, scanned_other)
135+
136+
nnx.update(layers, scanned_state)
137+
return final_carry

0 commit comments

Comments
 (0)