Skip to content

Commit 7c2c235

Browse files
committed
Apply cross-model review fixes to Qwen distribution tests
- Use the full serialized preset config for the Tier-3 live-preset init_kwargs instead of a dims-only allowlist, so real architecture flags are no longer silently dropped from the live-preset build. - Make the Tier-3 memory-budget cap tunable via the KERAS_HUB_DISTRIBUTION_TEST_MEM_BUDGET env var instead of a hardcoded, unreachable 300MB, so CI/larger machines can opt into full-scale verification. - Fix a file-handle leak in the preset config fetch (use `with open`). - Reword personal-machine narrative in comments/skip messages to be environment-neutral for the public repo. - Extend the Tier-3 memory-estimate formula to include the untied reverse_embeddings output-projection table and full GQA-scaled attention Q/K/V/O terms, matching the gpt_oss reference template. - Fix a "kerne" -> "kernel" typo and stale Llama-3-scale dims in the get_layout_map docstring's weight-shape table; replace with real qwen2.5_0.5b_en dims and add the missing reverse_embeddings row. - Fix keras.distribution.Device -> DeviceMesh and device_mesh.axis_name -> axis_names typos in get_layout_map's ValueError messages.
1 parent 8d7e71c commit 7c2c235

2 files changed

Lines changed: 98 additions & 59 deletions

File tree

keras_hub/src/models/qwen/qwen_backbone.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -258,34 +258,39 @@ def get_layout_map(
258258
for all the model weights.
259259
"""
260260
# The weight path and shape of the Qwen backbone is like below
261-
# token_embedding/embeddings (128256, 2048)
261+
# (dims shown for qwen2.5_0.5b_en: vocab=151936, hidden=896,
262+
# num_query_heads=14, num_key_value_heads=2, head_dim=64,
263+
# intermediate_dim=4864):
264+
# token_embedding/embeddings (151936, 896)
262265
# repeat block for decoder
263-
# transformer_layer_0/self_attention/query/kernel (2048, 32, 64)
264-
# transformer_layer_0/self_attention/key/kernel (2048, 8, 64)
265-
# transformer_layer_0/self_attention/value/kernel (2048, 8, 64)
266+
# transformer_layer_0/self_attention/query/kernel (896, 14, 64)
267+
# transformer_layer_0/self_attention/key/kernel (896, 2, 64)
268+
# transformer_layer_0/self_attention/value/kernel (896, 2, 64)
266269
# transformer_layer_0/self_attention/attention_output/kernel
267-
# (32, 64, 2048)
268-
# transformer_layer_0/self_attention_layernorm/scale (2048,)
270+
# (14, 64, 896)
271+
# transformer_layer_0/self_attention_layernorm/scale (896,)
269272
# transformer_layer_0/feedforward_intermediate_dense/kernel
270-
# (2048, 8192)
271-
# transformer_layer_0/feedforward_gate_dense/kernel (2048, 8192)
272-
# transformer_layer_0/feedforward_output_dense/kerne (8192, 2048)
273-
# transformer_layer_0/feedforward_layernorm/scale (2048,)
273+
# (896, 4864)
274+
# transformer_layer_0/feedforward_gate_dense/kernel (896, 4864)
275+
# transformer_layer_0/feedforward_output_dense/kernel (4864, 896)
276+
# transformer_layer_0/feedforward_layernorm/scale (896,)
277+
# (only present when tie_word_embeddings=False)
278+
# token_embedding/reverse_embeddings (896, 151936)
274279

275280
if not isinstance(device_mesh, keras.distribution.DeviceMesh):
276281
raise ValueError(
277282
"Invalid device_mesh type. Expected "
278-
f"`keras.distribution.Device`, got {type(device_mesh)}"
283+
f"`keras.distribution.DeviceMesh`, got {type(device_mesh)}"
279284
)
280285
if model_parallel_dim_name not in device_mesh.axis_names:
281286
raise ValueError(
282287
f"{model_parallel_dim_name} is not found in the "
283-
f"device_mesh.axis_names. {device_mesh.axis_name=}"
288+
f"device_mesh.axis_names. {device_mesh.axis_names=}"
284289
)
285290
if data_parallel_dim_name not in device_mesh.axis_names:
286291
raise ValueError(
287292
f"{data_parallel_dim_name} is not found in the "
288-
f"device_mesh.axis_names. {device_mesh.axis_name=}"
293+
f"device_mesh.axis_names. {device_mesh.axis_names=}"
289294
)
290295
# Note that it is possible to further config the mesh to be 3D, eg
291296
# (data, seq, model). We leave it as 2D for now for simplicity.

keras_hub/src/models/qwen/qwen_backbone_test.py

Lines changed: 80 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import gc
22
import json
3+
import os
34
import re
45

56
import keras
@@ -17,22 +18,23 @@
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.
3638
QWEN_0_5B_DIMS = {
3739
"source_preset": "qwen2.5_0.5b_en (real ratio, memory-scaled dims)",
3840
"vocabulary_size": 2048,
@@ -64,14 +66,14 @@
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.
7577
CAPPED_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

Comments
 (0)