Skip to content

fix(target-lm-head/dflash): fail closed on verifier-norm load and honour last_hidden_states_prenorm in DFlash - #162

Merged
yubofredwang merged 4 commits into
mainfrom
export/target-norm-failfast
Aug 9, 2026
Merged

fix(target-lm-head/dflash): fail closed on verifier-norm load and honour last_hidden_states_prenorm in DFlash#162
yubofredwang merged 4 commits into
mainfrom
export/target-norm-failfast

Conversation

@torchspec-bot

@torchspec-bot torchspec-bot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

What

Two related fixes to how the target model's final norm is loaded and applied when
last_hidden_states_prenorm is set.

1. TargetLMHead fails closed instead of silently dropping the norm

TargetLMHead.__init__ kept only config.text_config, so multimodal targets could not be
rebuilt by AutoModelForCausalLM.from_config and final-norm extraction raised. Both
_init_and_load_norm and _init_norm_structure swallowed that exception and left
self.norm = None, so a run configured with last_hidden_states_prenorm: true trained the
draft against unnormalised pre-norm hidden states with nothing louder than a warning.

  • Keep the outer config (model_config) for architecture introspection, alongside the text
    sub-config the module allocates against.
  • When introspection still fails, rebuild the norm from hidden_size/rms_norm_eps — every
    target seen so far ends in a plain RMSNorm.
  • When even that is impossible, raise. load_norm=True is a correctness requirement, and
    _init_norm_structure returning without a norm additionally desynchronises the rank-0
    parameter broadcast and deadlocks it.
  • Eagle3Trainer rejects a missing norm under last_hidden_states_prenorm on rank 0 and at
    the post-broadcast status check, and logs the norm state it actually ended up with rather
    than (with verifier norm), which only ever reflected the request.

2. DFlash honours last_hidden_states_prenorm

resolve_config defaults last_hidden_states_prenorm to True for inference_engine_type: vllm, but DFlashTrainer never passed load_norm through to TargetLMHead and never
applied a norm in _forward — so DFlash and DSpark runs against a vLLM target trained on
unnormalised hidden states while the resolved config said otherwise. This mirrors the Eagle3
path: load on rank 0, build the matching structure elsewhere, reconcile the status across
ranks before the parameter broadcast, and normalise under no_grad in _forward.

Tests

…unavailable

`TargetLMHead.__init__` kept only `config.text_config`, so multimodal targets
could not be rebuilt by `AutoModelForCausalLM.from_config` and final-norm
extraction raised. Both norm entry points swallowed that exception and left
`self.norm = None`, so a run configured with `last_hidden_states_prenorm: true`
silently trained the draft against unnormalised pre-norm hidden states — no
error, just wrong targets.

Retain the outer config for architecture introspection, and when introspection
still fails, rebuild the norm from `hidden_size`/`rms_norm_eps`; every target
seen so far ends in a plain RMSNorm. When even that is impossible, raise
instead of continuing: `load_norm=True` is a correctness requirement, and
`_init_norm_structure` silently returning without a norm additionally
desynchronises the rank-0 parameter broadcast and deadlocks it.

`Eagle3Trainer` now rejects a missing norm under `last_hidden_states_prenorm`
both on rank 0 and at the post-broadcast status check, and logs the norm state
it actually ended up with rather than `(with verifier norm)`, which only ever
reflected the request.

Signed-off-by: torchspec-bot <262938024+torchspec-bot@users.noreply.github.com>
…is set

`last_hidden_states_prenorm` defaults to True for the vLLM engine, but
`DFlashTrainer` never passed `load_norm` through to `TargetLMHead` and never
applied a norm in `_forward`. DFlash and DSpark runs against a vLLM target
therefore trained on unnormalised pre-norm hidden states while the resolved
config said otherwise.

Mirror the Eagle3 path: load the norm on rank 0, build the matching structure
on the other ranks so the parameter broadcast stays in sync, reconcile the
status across ranks before that broadcast, and normalise `last_hidden_states`
under `no_grad` in `_forward`.

Signed-off-by: torchspec-bot <262938024+torchspec-bot@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 773f9fa435

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/target/target_utils.py
…asting

Rank 0's fail-closed norm load raised inside `TargetLMHead.from_pretrained`,
before the status broadcast that the other ranks reach after building their own
norm structure. Those ranks then sat in that broadcast until the process-group
timeout instead of failing with the intended clean error.

Deduplicate `_init_target_lm_head` — already a near-verbatim copy in the Eagle3
and DFlash trainers, and the preceding commit added the norm handling to both —
into `load_synced_target_lm_head`, so this failure handling exists in one place.
Every rank now reports its local outcome through an `all_reduce(MIN)` before the
parameter broadcast; the rank that failed re-raises its own exception so the
type and traceback survive, and the others report which side failed. `MIN`
rather than a broadcast from rank 0 so that a follower failing in
`_init_norm_structure` or `AutoConfig.from_pretrained` aborts the job too,
instead of stranding rank 0 in the same way.

Drop the `has_norm` broadcast and its two recovery branches, which the new
all-reduce subsumes. With initialization failing closed, `norm is not None`
holds exactly when `load_norm` does, on every rank: rank 0 either loads the norm
or raises, followers either build the structure or raise, and `load_norm` comes
from the same resolved config everywhere. The broadcast could therefore only
confirm what each rank already knew, and the mismatch-recovery branches were
unreachable. The number of collectives before the parameter broadcast is
unchanged.

Signed-off-by: torchspec-bot <262938024+torchspec-bot@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf2ec4f1d1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

self.target_lm_head = load_synced_target_lm_head(
target_model_path,
load_norm=self._last_hs_prenorm,
lm_head_key=getattr(self.args, "lm_head_key", "lm_head.weight"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid requiring a norm for CE-only DFlash runs

For DFlash, last_hidden_states_prenorm defaults to true with the vLLM backend, but the DFlash model only consumes last_hidden_states when dflash_l1_loss_alpha > 0 (torchspec/models/dflash.py:398). This line now forces verifier-norm loading for ordinary CE-only DFlash startup, so a missing or unsupported norm_key can abort a run even though the norm is never used; gate load_norm on actually using last-hidden-state distillation rather than the prenorm flag alone.

Useful? React with 👍 / 👎.

…the target

`last_hidden_states_prenorm` defaults to True for the vLLM engine, so the
preceding commit made every DFlash run load the target's final norm. But DFlash
trains on hard-label cross entropy against `input_ids` and only reads
`last_hidden_states` for the L1 distribution term — and `dflash_l1_loss_alpha`
defaults to 0. An ordinary CE-only run therefore aborted on an unsupported
`norm_key` for a norm it would never apply.

Gate the load on the objective actually consuming the target's hidden states.
The predicate lives on the loss module, next to the branches it describes, so it
cannot drift from them: `DFlashModel` reports the L1 weight, `DSparkModel` adds
the confidence head, whose accept-rate target also comes from the verifier
distribution. Both now guard their own branches with it, and the trainer reads
it off the unwrapped model it already keeps in `self.dflash`.

`_forward` drops `last_hidden_states` under the same predicate rather than
forwarding them unnormalised. The models discard them anyway when it is false,
so no loss changes; but should a future branch read them without widening the
predicate, it meets the existing explicit `ValueError` instead of silently
training against unnormalised pre-norm activations — the failure this series
exists to close. It also skips a full-size host-to-device copy and an RMSNorm
per step on CE-only runs that still carry the tensor.

Log when a prenorm run skips the norm, since `load_synced_target_lm_head` would
otherwise report `not requested` and leave no way to tell that from a config
that never asked for it.

Signed-off-by: torchspec-bot <262938024+torchspec-bot@users.noreply.github.com>
@torchspec-bot
torchspec-bot force-pushed the export/target-norm-failfast branch from 3b6c73b to 8f270a9 Compare August 9, 2026 11:23
@yubofredwang
yubofredwang merged commit a8229bf into main Aug 9, 2026
2 checks passed
@yubofredwang
yubofredwang deleted the export/target-norm-failfast branch August 9, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants