11import gc
22import json
3+ import os
34import re
45
56import keras
1718# `get_file` call to the Tier-2 test body itself (that's what Tier 3,
1819# `test_layout_map_live_presets` below, is for).
1920#
20- # MEMORY NOTE: this local dev machine cannot load full-scale Qwen dims. What
21- # actually matters for the divisibility/sharding properties this tier tests is
22- # the RATIO of query heads to key/value heads and whether hidden/intermediate/
23- # vocab divide the mesh's model-axis sizes -- not the absolute parameter count.
24- # So these dims are scaled down by roughly 20-30x from the real Qwen2.5 presets
25- # (confirmed by fetching their live config.json files) while preserving each
26- # preset's EXACT real query:kv head ratio (14:2, 16:2, 28:4) and keeping
27- # head_dim modest (32). num_layers is always 1 -- layout rules are per-decoder-
28- # block regexes, so depth is irrelevant to spec matching/divisibility. The
29- # real head counts are kept verbatim (not rounded to a power of 2) so the
30- # 14-head and 28-head classes genuinely exercise the num_query_heads-not-
31- # divisible-by-8 skip path, while the 16-head class builds at every capped
32- # mesh shape. Full-scale real dims are exercised by
33- # `test_layout_map_live_presets` below, which has its own per-width-class
34- # memory-budget skip so it never attempts a full-scale build locally either --
35- # true full-scale verification happens offline on a machine with more RAM.
21+ # MEMORY NOTE: full-scale Qwen dims are impractical to build in
22+ # memory-constrained local environments. What actually matters for the
23+ # divisibility/sharding properties this tier tests is the RATIO of query
24+ # heads to key/value heads and whether hidden/intermediate/vocab divide the
25+ # mesh's model-axis sizes -- not the absolute parameter count. So these dims
26+ # are scaled down by roughly 20-30x from the real Qwen2.5 presets (confirmed
27+ # by fetching their live config.json files) while preserving each preset's
28+ # EXACT real query:kv head ratio (14:2, 16:2, 28:4) and keeping head_dim
29+ # modest (32). num_layers is always 1 -- layout rules are per-decoder-block
30+ # regexes, so depth is irrelevant to spec matching/divisibility. The real
31+ # head counts are kept verbatim (not rounded to a power of 2) so the 14-head
32+ # and 28-head classes genuinely exercise the num_query_heads-not-divisible-
33+ # by-8 skip path, while the 16-head class builds at every capped mesh shape.
34+ # Full-scale real dims are exercised by `test_layout_map_live_presets` below,
35+ # which has its own per-width-class memory-budget skip so it never attempts
36+ # a full-scale build locally either -- true full-scale verification happens
37+ # on a machine with more RAM or in CI.
3638QWEN_0_5B_DIMS = {
3739 "source_preset" : "qwen2.5_0.5b_en (real ratio, memory-scaled dims)" ,
3840 "vocabulary_size" : 2048 ,
6466 "head_dim" : 32 ,
6567}
6668
67- # Hard-capped mesh-shape list for this shared 37GB dev machine . The full
68- # 10-shape matrix from the testing-strategy doc is
69+ # Hard-capped mesh-shape list for memory-constrained local environments . The
70+ # full 10-shape matrix from the testing-strategy doc is
6971# 2x4, 1x8, 4x4, 8x8, 16x16, 2x2x2, 1x1x8, 2x2x4, 4x4x4, 4x4x8 -- shapes
7072# 8x8, 16x16, 4x4x4, 4x4x8 (64-256 virtual devices) are DELIBERATELY
71- # DROPPED here due to a demonstrated systemd-oomd OOM kill of the entire
72- # desktop app on this shared machine during an earlier attempt at this same
73- # pipeline. Do not attempt the dropped shapes even experimentally on this box
74- # -- revisiting them requires a dedicated or CI machine, not this one .
73+ # DROPPED here due to a demonstrated OOM kill during an earlier attempt at
74+ # this same pipeline on a memory-constrained machine. These shapes require a
75+ # dedicated or CI machine with more memory -- do not attempt them
76+ # experimentally in a constrained local environment .
7577CAPPED_MESH_SHAPES = [
7678 (2 , 4 ),
7779 (1 , 8 ),
@@ -262,8 +264,8 @@ def test_layout_map_mesh_shapes(self, dims, mesh_shape):
262264 # Untied so reverse_embeddings is present and covered by the sweep too.
263265 init_kwargs ["tie_word_embeddings" ] = False
264266 with distribution .scope ():
265- # bfloat16: a memory mitigation for this shared dev machine --
266- # spec assertions are dtype-independent.
267+ # bfloat16: a memory mitigation for memory-constrained local
268+ # environments -- spec assertions are dtype-independent.
267269 model = QwenBackbone (dtype = "bfloat16" , ** init_kwargs )
268270 _assert_qwen_shardings_and_coverage (self , model , layout_map )
269271 del model
@@ -279,9 +281,10 @@ def test_layout_map_live_presets(self):
279281 # Fetch every preset's config only (no weights), then dedupe by the
280282 # divisibility-relevant dims so width-classes that share a config
281283 # (e.g. base vs instruction-tuned variants of the same size) are only
282- # built once per mesh shape -- a memory/time necessity on this
283- # machine -- while every preset in the registry is still fetched and
284- # evaluated, preserving full registry coverage.
284+ # built once per mesh shape -- a memory/time necessity in
285+ # memory-constrained local environments -- while every preset in the
286+ # registry is still fetched and evaluated, preserving full registry
287+ # coverage.
285288 dim_keys = (
286289 "vocabulary_size" ,
287290 "num_query_heads" ,
@@ -294,7 +297,8 @@ def test_layout_map_live_presets(self):
294297 for preset in QwenBackbone .presets :
295298 try :
296299 path = get_file (preset , CONFIG_FILE )
297- cfg = json .load (open (path ))["config" ]
300+ with open (path ) as f :
301+ cfg = json .load (f )["config" ]
298302 except Exception as e :
299303 # A preset this account can't reach (e.g. an unaccepted
300304 # Kaggle license consent click-through) is logged, not
@@ -304,7 +308,7 @@ def test_layout_map_live_presets(self):
304308 # num_layers is forced to 1 below regardless of the real value --
305309 # layout rules are per-decoder-block regexes, so depth is
306310 # irrelevant to spec matching/divisibility, and 1 layer keeps
307- # build memory bounded on this shared machine .
311+ # build memory bounded in memory-constrained local environments .
308312 cfg = dict (cfg )
309313 cfg ["num_layers" ] = 1
310314 key = tuple (cfg .get (k ) for k in dim_keys )
@@ -361,30 +365,56 @@ def test_layout_map_live_presets(self):
361365 skip_reasons .append (reason )
362366 continue
363367
364- # Memory-budget guard: this shared dev machine cannot
365- # locally build full-scale presets. Estimate this
366- # width-class's single-decoder-block bf16 footprint
367- # (embedding table + one FFN block's 3 matrices, times a
368- # 3x safety margin for JAX/XLA transient copies during
369- # construction/resharding) and skip the actual build if it
370- # exceeds a conservative local threshold. The config-fetch,
371- # dedup, and divisibility-skip logic above still exercises
368+ # Memory-budget guard: memory-constrained local
369+ # environments cannot locally build full-scale presets.
370+ # Estimate this width-class's single-decoder-block bf16
371+ # footprint (embedding table + untied output-projection
372+ # table + one attention block's Q/K/V/O matrices + one
373+ # FFN block's 3 matrices, times a 3x safety margin for
374+ # JAX/XLA transient copies during construction/
375+ # resharding) and skip the actual build if it exceeds a
376+ # conservative local threshold. The config-fetch, dedup,
377+ # and divisibility-skip logic above still exercises
372378 # every registry preset either way; only the expensive
373379 # build+assert step is capped.
380+ hidden = cfg ["hidden_dim" ]
381+ inter = cfg ["intermediate_dim" ]
382+ num_kv_heads = cfg ["num_key_value_heads" ]
383+ # Attention projections map hidden_dim<->hidden_dim (q
384+ # and o), and hidden_dim<->(hidden_dim scaled by the
385+ # kv/query head ratio) for GQA's k and v -- none of the
386+ # four touch intermediate_dim, which is the FFN width
387+ # instead.
388+ kv_ratio = num_kv_heads / num_query_heads
374389 est_params = (
375- cfg ["vocabulary_size" ] * cfg ["hidden_dim" ]
376- + 3 * cfg ["hidden_dim" ] * cfg ["intermediate_dim" ]
390+ # Input embedding table.
391+ cfg ["vocabulary_size" ] * hidden
392+ # Untied output-projection table (reverse_embeddings)
393+ # -- this test always builds with
394+ # tie_word_embeddings=False below, so the weight
395+ # always exists and must be counted.
396+ + cfg ["vocabulary_size" ] * hidden
397+ + 2 * hidden * hidden # attention q/o
398+ + 2 * hidden * hidden * kv_ratio # attention k/v
399+ + 3 * hidden * inter # FFN (gate/intermediate/output)
377400 )
378401 est_bytes = est_params * 2 * 3 # bf16 * safety margin
379- max_local_bytes = 300 * 1024 * 1024 # 300MB
402+ max_local_bytes = int (
403+ os .environ .get (
404+ "KERAS_HUB_DISTRIBUTION_TEST_MEM_BUDGET" ,
405+ 300 * 1024 * 1024 ,
406+ )
407+ )
380408 if est_bytes > max_local_bytes :
381409 reason = (
382410 f"{ combo_label } : estimated build memory "
383411 f"~{ est_bytes / 1e9 :.2f} GB exceeds the "
384412 f"{ max_local_bytes / 1e6 :.0f} MB local safety "
385- "threshold on this shared, RAM-constrained dev "
386- "machine -- verify this width-class on a machine "
387- "with more RAM or in CI"
413+ "threshold for memory-constrained local "
414+ "environments -- verify this width-class on a "
415+ "machine with more RAM or in CI (override via "
416+ "the KERAS_HUB_DISTRIBUTION_TEST_MEM_BUDGET env "
417+ "var)"
388418 )
389419 skip_reasons .append (reason )
390420 continue
@@ -403,9 +433,13 @@ def test_layout_map_live_presets(self):
403433 distribution = keras .distribution .ModelParallel (
404434 layout_map = layout_map , batch_dim_name = "batch"
405435 )
406- init_kwargs = {
407- k : v for k , v in cfg .items () if k in dim_keys
408- }
436+ # `cfg` is a full serialized `get_config()` dict, which
437+ # always includes a `"dtype"` key (a serialized
438+ # dtype-policy dict) -- drop it so the explicit
439+ # `dtype="bfloat16"` override below doesn't collide with
440+ # a duplicate keyword argument. `name`/`trainable` pass
441+ # through harmlessly via **kwargs.
442+ init_kwargs = {k : v for k , v in cfg .items () if k != "dtype" }
409443 init_kwargs ["num_layers" ] = 1
410444 # Untied so reverse_embeddings is covered here too.
411445 init_kwargs ["tie_word_embeddings" ] = False
0 commit comments