Skip to content

feat(sft): require renderer-based tokenization#2988

Open
hubert-marek wants to merge 14 commits into
mainfrom
feat/sft-renderer-only-nodatafiles
Open

feat(sft): require renderer-based tokenization#2988
hubert-marek wants to merge 14 commits into
mainfrom
feat/sft-renderer-only-nodatafiles

Conversation

@hubert-marek

@hubert-marek hubert-marek commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Split from #2942 (part 1/7 of #2485) — renderer-only SFT tokenization, without the local data_files ingestion that ships separately.

Makes SFT tokenization renderer-only and deletes the broken incremental masking path:

  • Renderer-only SFT: build_training_sample with a hand-coded renderer replaces the incremental apply_chat_template masking path, which corrupted loss masks under position-dependent chat templates (e.g. Qwen3 stripping past thinking blocks across user turns). DefaultRenderer is rejected in setup_renderer (it falls back to incremental tokenization and doesn't fix the problem); fake data skips renderer construction so models without a hand-coded renderer can still benchmark step time. Non-assistant roles opt into the loss via the renderer's body-only path (content_sft_roles).
  • [renderer] config defaults to auto-resolution from the tokenizer name; template controls (e.g. enable_thinking) are set run-wide via the typed renderer config. Per-example chat_template_kwargs columns are ignored with a warning.
  • Null-safe message resolution: messages takes precedence over prompt/completion only when non-null — Arrow schema union adds messages: null to prompt/completion rows whenever other rows have a messages column, and the previous key-presence check silently resolved those rows to empty message lists.
  • _drop_null_fields strips Arrow phantom nulls before tool-call deserialization so genuine nulls inside argument strings survive.
  • Defaults: seq_len 256 (FakeDataConfig keeps 128), loss_impl liger_fused; CI + example reverse-text/wordle configs pin [renderer] name = "qwen3" (PrimeIntellect/* fine-tunes miss the auto-renderer's exact-match lookup).
  • The renderer is also built when only [val] uses real data (fake training data + validation would otherwise crash at the first val step), and the now-dead incremental-tokenization helpers (build_incremental_token_mask and friends) are deleted from utils/chat_template.py.

VLM SFT remains rejected at config here (validate_renderer_vs_vlm) — multimodal support lands later in the stack.

Tests

All 18 tests in test_sft_dataset.py pass:

18 passed, 3 warnings in 18.17s

Note

High Risk
This changes the core SFT data path and loss-mask semantics; misconfigured or unmapped renderers will fail validation or alter what tokens are trained, affecting training correctness for existing runs.

Overview
SFT now tokenizes exclusively through the renderers package — the incremental apply_chat_template / build_incremental_token_mask path is removed from SFTDataset and chat_template.py.

Training samples are built with build_training_sample (sampled-mask loss by default, optional content_sft_roles for non-assistant roles, ensure_final_stop=True). [renderer] defaults to auto and config validation rejects DefaultRenderer or unmapped models whenever real train/val data is used; fake-only runs can skip constructing a renderer. Dataset handling adds _drop_null_fields for Arrow phantom nulls, null-safe messages vs prompt/completion, and requires a renderer in setup_dataset for type = "sft".

Docs/examples shift to typed renderer config (e.g. Wordle RL uses prime-qwen3); new unit tests cover renderer validation and dataset behavior.

Reviewed by Cursor Bugbot for commit 041a1e5. Bugbot is set up for automated code reviews on this repo. Configure here.

samsja and others added 6 commits July 9, 2026 15:40
SFT tokenization now goes exclusively through the renderers library:
- build_training_sample with a hand-coded renderer replaces the
  incremental apply_chat_template masking path, which corrupted loss
  masks under position-dependent chat templates (e.g. Qwen3 stripping
  past thinking blocks across user turns). DefaultRenderer is rejected
  in setup_renderer; fake data skips renderer construction entirely.
- [renderer] config defaults to auto-resolution from the tokenizer;
  template controls (e.g. enable_thinking) are set run-wide via the
  typed renderer config.
- Non-assistant roles opt into the loss via the renderer's body-only
  path (content_sft_roles).
- _drop_null_fields strips Arrow phantom nulls before tool-call
  deserialization so genuine nulls inside argument strings survive.
- Null-check rather than key-check for messages vs prompt/completion
  resolution (Arrow schema union adds messages: null to prompt/
  completion rows).
- seq_len default 256 (FakeDataConfig keeps 128); loss_impl defaults
  to liger_fused; CI reverse-text configs pin [renderer] name=qwen3.

Split from #2942 (part 1/7 of #2485), renderer-only without data_files.
…own chat template

PrimeIntellect/Qwen3-0.6B and -1.7B ship a chat_template.jinja distinct
from Qwen3's, so pinning [renderer] name="qwen3" trained them with the
wrong template. setup_renderer now honors an explicit name="default"
(apply_chat_template with the checkpoint's own template) and only rejects
the silent auto-resolution fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renderer pairing is checked once in __init__ (tokenizer=None stays the
tests' passthrough mode) instead of raising mid-processing; the
chat_template_kwargs backward-compat warning is dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the OrchestratorConfig validator: renderer.name='auto' with a
model outside MODEL_RENDERER_MAP now fails config validation (and
--dry-run) instead of at trainer startup. The redundant runtime guard
in sft/train.py is removed; fake-data runs without validation stay
exempt since they build no renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hubert-marek hubert-marek marked this pull request as ready for review July 10, 2026 02:05
@hubert-marek hubert-marek requested a review from samsja July 10, 2026 02:06
@hubert-marek hubert-marek changed the title Feat/sft renderer only nodatafiles feat(sft): require renderer-based tokenization Jul 10, 2026
samsja
samsja previously approved these changes Jul 10, 2026
Comment thread packages/prime-rl-configs/src/prime_rl/configs/sft.py
Comment thread src/prime_rl/trainer/sft/data.py

@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: 36bc39b6f9

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +288 to +289
if model_id in MODEL_RENDERER_MAP:
return self

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 Reject auto renderers when a custom chat template is set

When an SFT config sets tokenizer.chat_template for a model that is in MODEL_RENDERER_MAP, this validator now accepts renderer.name='auto', but training will instantiate the hand-coded renderer instead of the tokenizer's apply_chat_template path. That silently ignores the custom template that setup_tokenizer loaded, whereas the previous default SFT tokenization path honored it; users need to be forced to choose [renderer] name='default' or an explicit matching renderer in this case.

Useful? React with 👍 / 👎.

Comment thread src/prime_rl/trainer/sft/data.py
Comment thread src/prime_rl/trainer/sft/data.py

@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: d97cf5f0ca

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/prime_rl/trainer/sft/train.py
Comment thread packages/prime-rl-configs/src/prime_rl/configs/sft.py
Comment thread src/prime_rl/trainer/sft/data.py
samsja
samsja previously approved these changes Jul 10, 2026
Comment thread examples/reverse_text/sft.toml Outdated
@hubert-marek hubert-marek force-pushed the feat/sft-renderer-only-nodatafiles branch from 9eeee0c to d97cf5f Compare July 10, 2026 20:11
@hubert-marek hubert-marek requested a review from samsja July 10, 2026 20:13
Bump renderers to the PrimeIntellect Qwen3 typed renderer, migrate the affected configs off default/qwen3 selections, and reject DefaultRenderer whenever SFT renders real samples.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread tests/unit/test_configs.py
The base PrimeIntellect Qwen3 checkpoints now auto-resolve to the typed renderer; only renamed fine-tunes keep an explicit selection.

Co-authored-by: Cursor <cursoragent@cursor.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: 839a543be4

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/prime_rl/trainer/sft/data.py Outdated
Comment thread src/prime_rl/trainer/sft/data.py
Defer to the renderer's sampled_mask by default so turn-closing role markers attributed to the next message stay trainable, and append the canonical stop token when a final assistant turn renders without one.

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

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 592743f. Configure here.

Comment thread src/prime_rl/trainer/sft/data.py
Comment thread packages/prime-rl-configs/src/prime_rl/configs/sft.py Outdated
Comment thread packages/prime-rl-configs/src/prime_rl/configs/sft.py Outdated
return self

model_id = self.tokenizer.name or self.model.name
if isinstance(self.renderer, AutoRendererConfig):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

in which case is it not instance of AutoRenderConfig ? shouldn;t it be just if self.renderer is not None ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It needs to resolve later by acutally checking if there is an renderer to be selected by auto

hubert-marek and others added 2 commits July 10, 2026 22:40
Bump renderers to main with ensure_final_stop and drop the trainer-side append.

Co-authored-by: Cursor <cursoragent@cursor.com>
These changes were unrelated to the renderer-only migration.

Co-authored-by: Cursor <cursoragent@cursor.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: 041a1e5598

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

role_to_mask=role_to_mask,
tools=tools,
content_sft_roles=content_sft_roles or None,
ensure_final_stop=True,

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 Pin renderers before using final-stop API

This call relies on a newer renderers.build_training_sample keyword, but the project dependency still permits an unconstrained renderers install (and the configs package only requires renderers>=0.1.8.dev28). In an install path that does not use the checked-out submodule, a satisfying renderers release whose signature lacks ensure_final_stop will raise TypeError on the first real SFT example; I verified the installed 0.1.8.dev57 API in this environment has no such parameter. Please bump/pin the required renderers version alongside this new call.

Useful? React with 👍 / 👎.

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