-
Notifications
You must be signed in to change notification settings - Fork 581
specdec: config_overrides for nested text_config checkpoints + load VLM-capable bases in merge_lora #2289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
specdec: config_overrides for nested text_config checkpoints + load VLM-capable bases in merge_lora #2289
Changes from all commits
d2943e0
ce56493
4ff57d5
b655c47
3308ffc
76860b1
8eed412
efb2ca2
4322e5e
91402fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |||||||||||||||||||
| import contextlib | ||||||||||||||||||||
| import copy | ||||||||||||||||||||
| import importlib.util | ||||||||||||||||||||
| import json | ||||||||||||||||||||
| import os | ||||||||||||||||||||
| import sys | ||||||||||||||||||||
| import warnings | ||||||||||||||||||||
|
|
@@ -36,6 +37,11 @@ | |||||||||||||||||||
| KIMI_K2_PACKAGE_NAME = "kimi_k2_temp" | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Attributes under which a checkpoint may nest its text-tower config. Mirrors | ||||||||||||||||||||
| # modelopt.torch.speculative.plugins.modeling_fakebase._VLM_CONFIG_ATTRS. | ||||||||||||||||||||
| NESTED_CONFIG_ATTRS = ["text_config", "llm_config"] | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| REMOVE_THINK_CHAT_TEMPLATE = ( | ||||||||||||||||||||
| "{% if '</think>' in content %}{% set content = content.split('</think>')[-1] %}{% endif %}" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
@@ -584,13 +590,47 @@ def enable_cp_ttt_patch(cp_size: int = 1): | |||||||||||||||||||
| modelopt.torch.speculative.plugins.hf_eagle.ENABLE_CP_TTT_PATCH = False | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| CONFIG_OVERRIDES_HELP = ( | ||||||||||||||||||||
| "JSON object of config fields to override on the model config and its text_config before " | ||||||||||||||||||||
| "instantiation, e.g. '{\"num_hidden_layers\": 36}'. Needed for checkpoints whose nested " | ||||||||||||||||||||
| "text_config dims don't propagate to the parent config." | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def parse_config_overrides(raw: str | None) -> dict | None: | ||||||||||||||||||||
| """Parse a ``--config_overrides`` CLI value into a dict, or ``None`` if not supplied. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Rejects malformed JSON and non-object payloads here, with an actionable message, rather than | ||||||||||||||||||||
| letting them surface later as a raw ``JSONDecodeError`` or an ``AttributeError`` from deep | ||||||||||||||||||||
| inside model loading. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| if not raw: | ||||||||||||||||||||
| return None | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| # Reject the JSON5-ish constants Python's json accepts by default: NaN/Infinity | ||||||||||||||||||||
| # would sail through as floats and land on a config field as a dimension. | ||||||||||||||||||||
| def _reject(const): | ||||||||||||||||||||
| raise ValueError(f"--config_overrides contains non-finite value {const!r}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| parsed = json.loads(raw, parse_constant=_reject) | ||||||||||||||||||||
| except json.JSONDecodeError as e: | ||||||||||||||||||||
| raise ValueError(f"--config_overrides is not valid JSON: {e}") from e | ||||||||||||||||||||
| if not isinstance(parsed, dict): | ||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||
| f"--config_overrides must be a JSON object mapping field names to values, got " | ||||||||||||||||||||
| f"{type(parsed).__name__}: {raw!r}" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| return parsed | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def load_vlm_or_llm( | ||||||||||||||||||||
| model_name_or_path: str, | ||||||||||||||||||||
| use_fake_base: bool = False, | ||||||||||||||||||||
| use_offline_training: bool = False, | ||||||||||||||||||||
| dtype: str | torch.dtype | None = None, | ||||||||||||||||||||
| device_map: str | None = None, | ||||||||||||||||||||
| trust_remote_code: bool = False, | ||||||||||||||||||||
| config_overrides: dict | None = None, | ||||||||||||||||||||
| ): | ||||||||||||||||||||
| """Load a VLM or LLM. Returns the model. | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -605,26 +645,84 @@ def load_vlm_or_llm( | |||||||||||||||||||
| dtype: dtype to use when loading the model. | ||||||||||||||||||||
| device_map: Device map passed to ``from_pretrained``. | ||||||||||||||||||||
| trust_remote_code: Whether to trust remote code. | ||||||||||||||||||||
| config_overrides: Optional config field overrides applied to the model config and | ||||||||||||||||||||
| its ``text_config`` before instantiation (e.g. to correct dims that don't | ||||||||||||||||||||
| propagate from a checkpoint's nested text config). | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _warn_overrides_on_fake_base(): | ||||||||||||||||||||
| # FakeBaseModel.from_source re-reads the checkpoint config itself, so overrides applied | ||||||||||||||||||||
| # here never reach it -- but it is not silently wrong: from_source resolves dims from the | ||||||||||||||||||||
| # nested text_config/llm_config first (modeling_fakebase._VLM_CONFIG_ATTRS), which is the | ||||||||||||||||||||
| # very problem config_overrides exists to work around, so this path is already correct | ||||||||||||||||||||
| # without them. Warn rather than raise: main.py forwards config_overrides unconditionally, | ||||||||||||||||||||
| # so hard-failing would leave a single recipe unable to run offline at all. | ||||||||||||||||||||
| if config_overrides: | ||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||
| "config_overrides is ignored on the FakeBaseModel path: from_source rebuilds the " | ||||||||||||||||||||
| "config from the checkpoint, reading dims from the nested text_config/llm_config " | ||||||||||||||||||||
| "directly, so the overrides are not needed there.", | ||||||||||||||||||||
| stacklevel=2, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if use_offline_training and use_fake_base: | ||||||||||||||||||||
| _warn_overrides_on_fake_base() | ||||||||||||||||||||
| from modelopt.torch.speculative.plugins.modeling_fakebase import FakeBaseModel | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return FakeBaseModel.from_source(model_name_or_path, trust_remote_code=trust_remote_code) | ||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Import the transformers-cosmos3 plugin if available: it registers the `cosmos3_omni` | ||||||||||||||||||||
| # architecture with AutoConfig on import, so the from_pretrained below recognizes it. | ||||||||||||||||||||
| with contextlib.suppress(ImportError): | ||||||||||||||||||||
| import transformers_cosmos3 # noqa: F401 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| model_config = transformers.AutoConfig.from_pretrained( | ||||||||||||||||||||
| model_name_or_path, | ||||||||||||||||||||
| trust_remote_code=trust_remote_code, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Apply caller-supplied config corrections to the parent config and every nested config | ||||||||||||||||||||
| # (some checkpoints don't propagate the nested dims up to the parent). | ||||||||||||||||||||
| if config_overrides: | ||||||||||||||||||||
| # Cover every nested attribute VLM detection accepts, not just text_config: an | ||||||||||||||||||||
| # llm_config-nesting checkpoint mirrors the fields on the parent as None, so an override | ||||||||||||||||||||
| # would land on the parent, count as "applied", and never reach the real text tower. | ||||||||||||||||||||
| targets = [ | ||||||||||||||||||||
| cfg | ||||||||||||||||||||
| for cfg in ( | ||||||||||||||||||||
| model_config, | ||||||||||||||||||||
| *(getattr(model_config, a, None) for a in NESTED_CONFIG_ATTRS), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| if cfg is not None | ||||||||||||||||||||
| ] | ||||||||||||||||||||
| unmatched = [] | ||||||||||||||||||||
| for key, value in config_overrides.items(): | ||||||||||||||||||||
| applied = False | ||||||||||||||||||||
| for cfg_obj in targets: | ||||||||||||||||||||
| if hasattr(cfg_obj, key): | ||||||||||||||||||||
| setattr(cfg_obj, key, value) | ||||||||||||||||||||
|
Comment on lines
+686
to
+703
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility] A mistyped or non-applicable override key is silently dropped, reproducing the exact bug this flag exists to fix.
Suggest failing loudly when a key matched nothing: if config_overrides:
targets = [c for c in (model_config, getattr(model_config, "text_config", None)) if c]
for key, value in config_overrides.items():
applied = [c for c in targets if hasattr(c, key)]
if not applied:
raise ValueError(
f"config_overrides key '{key}' is not a field of {type(model_config).__name__} "
"or its text_config."
)
for cfg_obj in applied:
setattr(cfg_obj, key, value)Related: since the loop writes to both configs unconditionally, an override for a name that means different things at the two levels (e.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8eed412. Scoped the guard to the two |
||||||||||||||||||||
| applied = True | ||||||||||||||||||||
| if not applied: | ||||||||||||||||||||
| unmatched.append(key) | ||||||||||||||||||||
| if unmatched: | ||||||||||||||||||||
| # Silently skipping a key would hand back a wrong-shaped model while appearing to | ||||||||||||||||||||
| # have applied the override -- the exact failure this option exists to correct. | ||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||
| f"config_overrides key(s) {sorted(unmatched)} matched no field on the model " | ||||||||||||||||||||
| f"config (model_type={getattr(model_config, 'model_type', None)!r}) or its " | ||||||||||||||||||||
| "text_config. Check for typos." | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Detect VLMs: either "vl" in model_type (e.g. "llava") or has a nested text config | ||||||||||||||||||||
| # (e.g. Mistral3Config with model_type="mistral3" and text_config attribute). | ||||||||||||||||||||
| _is_vlm = "vl" in model_config.model_type.lower() or any( | ||||||||||||||||||||
| getattr(model_config, attr, None) is not None for attr in ["text_config", "llm_config"] | ||||||||||||||||||||
| getattr(model_config, attr, None) is not None for attr in NESTED_CONFIG_ATTRS | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if _is_vlm and use_offline_training: | ||||||||||||||||||||
| # For VLMs in offline training, FakeBaseModel loads only embed_tokens + lm_head | ||||||||||||||||||||
| # and auto-detects VLM weight key layouts (e.g. "language_model.model.embed_tokens"). | ||||||||||||||||||||
| _warn_overrides_on_fake_base() | ||||||||||||||||||||
| from modelopt.torch.speculative.plugins.modeling_fakebase import FakeBaseModel | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return FakeBaseModel.from_source(model_name_or_path, trust_remote_code=trust_remote_code) | ||||||||||||||||||||
|
|
@@ -641,13 +739,41 @@ def load_vlm_or_llm( | |||||||||||||||||||
| model_cls = transformers.AutoModelForCausalLM | ||||||||||||||||||||
|
|
||||||||||||||||||||
| extra = {} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Cosmos3 omni checkpoints: the transformers-cosmos3 plugin registers only the config | ||||||||||||||||||||
| # (cosmos3_omni) with AutoConfig, never a model under the Auto* maps, so dispatch to its | ||||||||||||||||||||
| # Cosmos3ForConditionalGeneration (a Qwen3-VL subclass) directly. The unused vision | ||||||||||||||||||||
| # tower has mismatched dims vs the text-only use, so ignore those on load. | ||||||||||||||||||||
| if getattr(model_config, "model_type", None) == "cosmos3_omni": | ||||||||||||||||||||
| from transformers_cosmos3 import Cosmos3ForConditionalGeneration | ||||||||||||||||||||
|
Comment on lines
+747
to
+748
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] This The soft import at line ~672 is A wrapped message points at the install: if getattr(model_config, "model_type", None) == "cosmos3_omni":
try:
from transformers_cosmos3 import Cosmos3ForConditionalGeneration
except ImportError as e:
raise ImportError(
"Loading a cosmos3_omni checkpoint requires the transformers-cosmos3 package, "
"which registers only the config with AutoConfig -- the model class must be "
"imported directly. Install it to load this checkpoint."
) from eUnrelated and trivial: the |
||||||||||||||||||||
|
|
||||||||||||||||||||
| model_cls = Cosmos3ForConditionalGeneration | ||||||||||||||||||||
| extra["ignore_mismatched_sizes"] = True | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility]
Concretely: The second-order effect is worse in Suggested fix — keep the escape hatch but verify what it actually let through, e.g. request the loading info and fail on anything outside the vision prefix: model, loading_info = model_cls.from_pretrained(
model_name_or_path,
config=model_config if pass_config else None,
trust_remote_code=trust_remote_code,
torch_dtype=dtype,
device_map=device_map,
output_loading_info=True,
**extra,
)
unexpected = [k for k, *_ in loading_info.get("mismatched_keys", ()) if not _is_vision_key(k)]
if unexpected:
raise ValueError(
f"Checkpoint weights {unexpected[:8]} were re-initialized because their shapes "
f"disagree with the config. If you passed config_overrides, one of the values is "
f"wrong for this checkpoint."
)( |
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Pass our config object only when we had to modify it (overrides) or when the model class | ||||||||||||||||||||
| # needs the plugin-built config; otherwise let from_pretrained build its own, exactly as before. | ||||||||||||||||||||
| pass_config = bool(config_overrides) or "ignore_mismatched_sizes" in extra | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Capture the true depth before any zeroing below, since it is restored after load. | ||||||||||||||||||||
| orig_num_hidden_layers = getattr(model_config, "num_hidden_layers", None) | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The Before this change the offline path did Reachability is low (any decoder config reaching this branch normally has
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| if use_offline_training: | ||||||||||||||||||||
| extra["num_hidden_layers"] = 0 | ||||||||||||||||||||
| if hasattr(model_config, "layer_types"): | ||||||||||||||||||||
| extra["layer_types"] = [] | ||||||||||||||||||||
| if pass_config: | ||||||||||||||||||||
| # from_pretrained only forwards unrecognized kwargs into the config when it builds | ||||||||||||||||||||
| # that config itself. Given a PretrainedConfig instance it deep-copies it and leaves | ||||||||||||||||||||
| # the rest in model_kwargs, so num_hidden_layers=0 would never reach the config and | ||||||||||||||||||||
| # the full model would be materialized. Set the fields on the config directly. | ||||||||||||||||||||
| model_config.num_hidden_layers = 0 | ||||||||||||||||||||
| if hasattr(model_config, "layer_types"): | ||||||||||||||||||||
| model_config.layer_types = [] | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| extra["num_hidden_layers"] = 0 | ||||||||||||||||||||
| if hasattr(model_config, "layer_types"): | ||||||||||||||||||||
| extra["layer_types"] = [] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| model = model_cls.from_pretrained( | ||||||||||||||||||||
| model_name_or_path, | ||||||||||||||||||||
| config=model_config if pass_config else None, | ||||||||||||||||||||
| trust_remote_code=trust_remote_code, | ||||||||||||||||||||
| torch_dtype=dtype, | ||||||||||||||||||||
| device_map=device_map, | ||||||||||||||||||||
|
|
@@ -656,7 +782,7 @@ def load_vlm_or_llm( | |||||||||||||||||||
|
|
||||||||||||||||||||
| if use_offline_training: | ||||||||||||||||||||
| # Preserve the original layer count since we loaded with num_hidden_layers=0 | ||||||||||||||||||||
| model.config.num_orig_hidden_layers = model_config.num_hidden_layers | ||||||||||||||||||||
| model.config.num_orig_hidden_layers = orig_num_hidden_layers | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return model | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[SUGGESTION] The non-finite guard has a hole:
parse_constantonly fires on the literal tokensNaN/Infinity/-Infinity. Overflowing decimal literals go throughparse_float(plainfloat()), so--config_overrides '{"num_hidden_layers": 1e999}'still yieldsinfand lands on a config field as a dimension — the exact outcome this callback was added to prevent.Since the values here are all config scalars, a post-parse check over the values is both simpler and complete, and it also catches the adjacent case of a wrong-typed value (
{"num_hidden_layers": "36"}currently sets astron the config, which surfaces later as an opaque error inside module construction):with
import mathat the top; theparse_constant=_rejectclosure can then go away.