Skip to content

[tinker] 17/n towards Kimi K2.6: run Tinker API forward requests through the training loss path - #2068

Open
casper-hansen wants to merge 1 commit into
NovaSky-AI:mainfrom
casper-hansen:casper/kimi-17-forward-loss-path
Open

[tinker] 17/n towards Kimi K2.6: run Tinker API forward requests through the training loss path#2068
casper-hansen wants to merge 1 commit into
NovaSky-AI:mainfrom
casper-hansen:casper/kimi-17-forward-loss-path

Conversation

@casper-hansen

Copy link
Copy Markdown
Contributor

Part of the Kimi K2.6/K2.7 series. Standalone. This is the second piece salvaged from #2030 (closed as superseded by #2021), as flagged there.

What

_forward_single_model_batch in the SkyRL-Train backend now routes FORWARD requests through the same loss pipeline as forward_backward (the dispatch's loss-path forward with forward_only semantics) whenever the request carries a loss_fn and the role is not critic — the tinker FORWARD request always carries one. Validation and loss normalization are shared with forward_backward. Critic and loss-less callers keep the plain worker forward.

Why

The loss-less worker forward takes Megatron's inference route, which bypasses training-only transforms — fake-INT4 QAT weight quantization (#1862) and the fused LM-head logprob (#1841). Its logprobs therefore diverge from what gradients are computed against.

On our Kimi K2.7-Code INT4 QAT runs through the Tinker API this was not cosmetic: forward() silently scored the unquantized weights, so client-side importance-sampling corrections computed from FORWARD logprobs disagreed with the training-side logprobs (a persistent rollout/train logprob gap in the #1880 metrics), and eval losses measured through FORWARD were skewed relative to training loss. With FORWARD on the loss path, the same batch scored via forward() and via forward_backward() produces matching logprobs.

Made with Cursor

The FORWARD endpoint dispatched to the loss-less worker forward, which
takes Megatron's inference route and bypasses training-only transforms:
fake-INT4 QAT weight quantization and the fused LM-head logprob. Its
logprobs therefore diverge from what forward_backward computes gradients
against — on our Kimi K2.7 INT4-QAT runs the mismatch showed up as a
persistent rollout/train logprob gap in importance-sampling metrics and
skewed eval losses, because forward() silently scored the *unquantized*
weights.

The tinker FORWARD request always carries a loss_fn; use it (policy
roles): validate + normalize exactly like forward_backward, then call
the dispatch's loss-path forward (forward_only). Critic and loss-less
callers keep the plain forward.

Co-authored-by: Cursor <cursoragent@cursor.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates the _forward_single_model_batch method in skyrl_train_backend.py to ensure that the forward pass runs the same pipeline as the forward-backward pass for non-critic roles by utilizing the loss function and its configuration. This prevents logprobs from diverging due to bypassed training-only transforms. The reviewer suggests adding a warning log when mixed loss functions are detected in a single batch, as only the first loss function is currently applied to the entire batch.

Comment on lines +957 to +960
loss_fn = prepared_batch.all_loss_fns[0] if prepared_batch.all_loss_fns else None
loss_fn_config = next((c for c in prepared_batch.all_loss_fn_configs if c is not None), None)
if role != "critic" and loss_fn is not None:
self._validate_batch_role_and_loss(role, loss_fn)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When a batch contains mixed loss functions (or a mix of loss-bearing and loss-less requests), only the first loss function is used for the entire batch. To prevent silent issues and maintain consistency with _forward_backward_single_model_batch, we should log a warning when mixed loss functions are detected in the batch.

Suggested change
loss_fn = prepared_batch.all_loss_fns[0] if prepared_batch.all_loss_fns else None
loss_fn_config = next((c for c in prepared_batch.all_loss_fn_configs if c is not None), None)
if role != "critic" and loss_fn is not None:
self._validate_batch_role_and_loss(role, loss_fn)
loss_fn = prepared_batch.all_loss_fns[0] if prepared_batch.all_loss_fns else None
if loss_fn is not None and len(set(prepared_batch.all_loss_fns)) > 1:
logger.warning(
"SkyRL backend received mixed loss functions %s in one batch; using '%s' for all",
set(prepared_batch.all_loss_fns),
loss_fn,
)
loss_fn_config = next((c for c in prepared_batch.all_loss_fn_configs if c is not None), None)
if role != "critic" and loss_fn is not None:
self._validate_batch_role_and_loss(role, loss_fn)

@avigyabb
avigyabb self-requested a review August 20, 2026 21:33
@avigyabb

Copy link
Copy Markdown
Collaborator

Hey Casper, thanks again for this PR! Was wondering if you could explain how you got the loss-less worker forward to bypass QAT? Looking at the code, it seems like both loss/loss-less paths should use QAT? Here are some of my notes on the function calls:

MegatronPolicyWorker.init_model() [megatron_worker.py]

  • self._maybe_setup_fake_int4_qat() → patches TEGroupedLinear._get_weight_tensors() to quantize then dequantize
  • self.init_configs() [megatron_worker.py]
    • AutoBridge.from_hf_pretrained() - converts an HF checkpoint into a megatron model recipe
    • bridge.to_megatron_provider() - produces a megatron model factory
    • provider.moe_grouped_gemm = True - causes Megatron to build experts using TEGroupedLinear implementation
  • self.actor_module = self.make_megatron_module()
  • self.model = MegatronModelWrapper(actor_module=self.actor_module, …)

_forward_single_model_batch() [skyrl_train_backend.py]

  • self._dispatch.forward() - different depending on if loss function exists
    • MegatronPolicyWorker.forward() [megatron_worker.py]
      • self._forward_logprobs()
        • self.model.forward() - use QAT

(From my previous comment on #2030) Maybe there’s another reason why the logprobs diverge (and if so, we should probably update the comment)? What do you think?

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