Skip to content

Commit 08b5aaf

Browse files
committed
fix(litertlm): minor code issues from review
- Skip populate_function_gemma_metadata when llm_model_type is an explicit override, mirroring litert-torch's override behavior. - Handle NumPy integer axes (np.int64/32) in _patched_amax. - Update Falcon xfail reason to current torch.fx _ModuleNotInstalledAsSubmoduleError. - Update Gemma3n xfail reason's shape numbers to current [3,8,8,16] strides (1024,8,1,64) -> [192,16].
1 parent 7d52390 commit 08b5aaf

7 files changed

Lines changed: 54 additions & 21 deletions

File tree

keras_hub/src/models/falcon/falcon_causal_lm_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,9 @@ def test_litert_export(self):
178178
@pytest.mark.xfail(
179179
strict=True,
180180
reason=(
181-
"torch.export tracing failure: broadcast shape mismatch "
182-
"[1,2,1,8] vs [1,2,8,12] at export time (not a runtime gap); "
181+
"torch.export tracing failure: "
182+
"torch.fx._ModuleNotInstalledAsSubmoduleError in FalconAttention "
183+
"(module not registered as a submodule during tracing); "
183184
"under diagnosis"
184185
),
185186
)

keras_hub/src/models/gemma3n/gemma3n_causal_lm_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,10 @@ def test_gqa_fit(self):
437437
strict=True,
438438
reason=(
439439
"Gemma3n LiteRT-LM export fails in litert-torch conversion: "
440-
"aten.view shape/stride mismatch ([3,64,8] strides (512,1,64) -> "
441-
"[192,8]) during forward_prefill decomposition. Pre-existing "
442-
"export bug, not a numeric-parity gap; blocks multimodal numeric "
443-
"wiring (WS3.3). Under diagnosis."
440+
"aten.view shape/stride mismatch ([3,8,8,16] strides "
441+
"(1024,8,1,64) -> [192,16]) during forward_prefill decomposition. "
442+
"Pre-existing export bug, not a numeric-parity gap; blocks "
443+
"multimodal numeric wiring (WS3.3). Under diagnosis."
444444
),
445445
)
446446
def test_litertlm_export(self):

keras_hub/src/tests/test_case.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,8 @@ def _verify_litertlm_multimodal_numerics(
11831183
# call (no `sampler_config`), matching what the family tests
11841184
# under verification actually export.
11851185
sampler_config=None,
1186+
# The harness never passes an `llm_model_type` override.
1187+
model_type_overridden=False,
11861188
)
11871189
prefill_inputs = _export._build_prefill_inputs(plan)[prefill_seq_len]
11881190

keras_hub/src/utils/litertlm/export.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ class ExportPlan:
438438
tokens_per_image: int | None
439439
separate_vision_encoder: bool
440440
sampler_config: object | None
441+
model_type_overridden: bool
441442

442443

443444
def _build_prefill_inputs(plan):
@@ -800,6 +801,7 @@ def _assemble_bundle(
800801
vision_cfg=plan.vision_cfg,
801802
audio_cfg=plan.audio_cfg,
802803
sampler_config=plan.sampler_config,
804+
model_type_overridden=plan.model_type_overridden,
803805
)
804806

805807
litert_lm_builder = _import_litert_lm_builder()
@@ -1226,6 +1228,7 @@ def export_to_litertlm(
12261228
tokens_per_image=tokens_per_image,
12271229
separate_vision_encoder=separate_vision_encoder,
12281230
sampler_config=sampler_config,
1231+
model_type_overridden=llm_model_type is not None,
12291232
)
12301233

12311234
with _cpu_default_device_scope():
@@ -1502,6 +1505,7 @@ def _build_llm_metadata(
15021505
vision_cfg=None,
15031506
audio_cfg=None,
15041507
sampler_config=None,
1508+
model_type_overridden=False,
15051509
):
15061510
"""Serialize an ``LlmMetadata`` protobuf to *path*."""
15071511
# The protobuf lives under an internal-looking subpackage of
@@ -1561,11 +1565,16 @@ def _build_llm_metadata(
15611565
spec.populate_audio_metadata(meta, audio_cfg)
15621566

15631567
# Populate function-calling fields for specs that declare them (currently
1564-
# only `FunctionGemmaSpec`, selected via the `llm_model_type` override).
1568+
# only `FunctionGemmaSpec`, reached via tokenizer auto-detection).
15651569
# A base-class no-op for every other family, so this adds nothing to
15661570
# existing model types -- the same convention as the vision/audio hooks
1567-
# above.
1568-
spec.populate_function_gemma_metadata(meta)
1571+
# above. Skipped when `llm_model_type` is an explicit caller override:
1572+
# litert-torch skips its model-specific metadata builder whenever
1573+
# `litert_lm_model_type_override` is set
1574+
# (`export_hf/core/litert_lm_builder.py` ~296), so an override yields the
1575+
# bare `llm_model_type` oneof with no model-specific fields.
1576+
if not model_type_overridden:
1577+
spec.populate_function_gemma_metadata(meta)
15691578

15701579
# Sampler defaults are written only when the caller explicitly passes a
15711580
# `sampler_config`; otherwise the field is omitted so the runtime picks

keras_hub/src/utils/litertlm/model_specs.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -558,10 +558,13 @@ def populate_function_gemma_metadata(self, meta):
558558
Default: no-op. Only ``FunctionGemmaSpec`` (the
559559
``function_gemma_instruct_270m`` preset) overrides this to fill the
560560
``FunctionGemma`` proto's function-calling fields; every other family
561-
leaves the field block untouched. Called unconditionally by
562-
``_build_llm_metadata`` right after the ``llm_model_type`` oneof is
563-
selected -- the same base-no-op convention ``populate_vision_metadata``
564-
/ ``populate_audio_metadata`` use.
561+
leaves the field block untouched. Called by ``_build_llm_metadata``
562+
right after the ``llm_model_type`` oneof is selected -- the same
563+
base-no-op convention ``populate_vision_metadata`` /
564+
``populate_audio_metadata`` use -- except when ``llm_model_type`` was
565+
an explicit caller override, mirroring litert-torch skipping its
566+
model-specific metadata builder on override
567+
(``export_hf/core/litert_lm_builder.py`` ~296).
565568
"""
566569
del meta
567570

keras_hub/src/utils/litertlm/traceable_ops.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import contextlib
1717
import unittest.mock
1818

19+
import numpy as np
1920
import torch
2021
from keras.src import backend
2122
from keras.src.backend.common import dtypes as keras_dtypes
@@ -489,6 +490,10 @@ def _patched_amax(x, axis=None, keepdims=False):
489490
unchanged, so this is a no-op transform outside the triggering case.
490491
"""
491492
x = torch_core.convert_to_tensor(x)
493+
# NumPy integer axes (e.g. ``np.int64`` from shape arithmetic) are not
494+
# Python ``int``; normalize so they take the same traceable path.
495+
if isinstance(axis, np.integer):
496+
axis = int(axis)
492497
if axis is not None and isinstance(axis, int) and x.ndim == 4:
493498
return torch.max(x, dim=axis, keepdim=keepdims).values
494499
return _ORIGINAL_AMAX(x, axis=axis, keepdims=keepdims)

keras_hub/src/utils/litertlm/traceable_ops_test.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -439,22 +439,35 @@ def test_amax_matches_original(self):
439439
(torch.randn(7), 0, True), # 1-D (fallthrough)
440440
(torch.randn(2, 2, 3, 4, 5), -1, True), # 5-D (fallthrough)
441441
(torch.randn(2, 3, 4, 5), (2, 3), True), # 4-D tuple (fallthrough)
442+
(torch.randn(2, 3, 4, 5), np.int64(-1), True), # np.int64 axis
443+
(torch.randn(2, 3, 4, 5), np.int32(1), True), # np.int32 axis
442444
]
443445
for x, axis, keepdims in cases:
444446
with self.subTest(
445447
shape=tuple(x.shape), axis=axis, keepdims=keepdims
446448
):
447-
original = torch_backend_numpy.amax(
448-
x, axis=axis, keepdims=keepdims
449-
)
450449
patched = traceable_ops._patched_amax(
451450
x, axis=axis, keepdims=keepdims
452451
)
453-
self.assertEqual(tuple(original.shape), tuple(patched.shape))
454-
# The reformulation is the identical reduction; values must be
455-
# bit-identical, not merely close (a silent numeric drift here
456-
# would corrupt attention-sink softmax stabilization).
457-
self.assertTrue(torch.equal(original, patched))
452+
# The original Keras amax cannot handle NumPy integer axes
453+
# (raises "truth value of an empty array is ambiguous"); for
454+
# those cases verify only that the patched version works and
455+
# matches torch.max directly.
456+
if isinstance(axis, np.integer):
457+
expected = torch.max(
458+
x, dim=int(axis), keepdim=keepdims
459+
).values
460+
self.assertTrue(torch.equal(expected, patched))
461+
else:
462+
original = torch_backend_numpy.amax(
463+
x, axis=axis, keepdims=keepdims
464+
)
465+
self.assertEqual(tuple(original.shape), tuple(patched.shape))
466+
# The reformulation is the identical reduction; values must
467+
# be bit-identical, not merely close (a silent numeric
468+
# drift here would corrupt attention-sink softmax
469+
# stabilization).
470+
self.assertTrue(torch.equal(original, patched))
458471

459472
def test_amax_public_ops_max_4d_matches(self):
460473
# The public keras op that GPT-OSS attention actually calls.

0 commit comments

Comments
 (0)