feat(ruler): add RULER long-context benchmark (datasets + tasks)#11
feat(ruler): add RULER long-context benchmark (datasets + tasks)#11Mea1Ma wants to merge 106 commits into
Conversation
ethan-scitix
left a comment
There was a problem hiding this comment.
Review: RULER benchmark onboarding
The port is faithful — I verified the generation logic against NVIDIA RULER at the pinned SHA, and the 6-length Qwen3-8B run reproduces published recall numbers closely (cwe@16k 93.5 vs 93.2, vt/niah ~100). Requesting changes on packaging and structure before merge.
Blocking
- Bundled corpus doesn't belong in the repo.
sieval/datasets/_data/paul_graham_essays/PaulGrahamEssays.json.gz(1.1MB) should not ship in-tree. RULER's corpus isn't an official artifact — NVIDIA ships only a URL list + scraper, so this.gzis just one local scrape frozen. Host one copy (HF@sha, or any immutable/public OSS object viaurl:) and keepscripts/gen_paul_graham_essays.pyas the BYO/regeneration path. The loader is host-agnostic once the file lands in the data dir. - Overlaps with the still-open #9. This branch re-includes #9's
downloaders/{base,local,url}.py+ tests verbatim. Land theurl:gzip fix via #9; rebase this PR on top and drop those files.local:itself should become true BYO (no in-wheel bundling;download= no-op + instructions) rather than copy-from-package. nltkmissing from therulerextra.pyproject.toml:39liststiktoken/wonderwords/numpy/scipybut notnltk, which the default niah/vt essay path imports (_common.py:71,ruler_niah.py:285,ruler_vt.py:315).check_dep_coveragepasses only becausenltkis inifeval; a cleanpip install sieval[ruler]breaks at load. Addnltkto therulergroup.
Structure (the bigger one)
- Collapse to 1 dataset + 1 task, mirroring MMLU. The 5 datasets / 5 empty task classes + the new
leaderboard ruler-avgcommand are all symptoms of the same gap. MMLU already models 57 subtasks as one dataset with aSubjectcolumn and aggregates per-group inreport()(mmlu_0shot_gen.py:92,102). Do the same: oneRulerDatasetemitting asubtaskcolumn, oneRulerTaskwhosereport()groups by subtask (right matcher per group —string_match_allvsstring_match_part) and emits per-subtask scores + the headline mean. The context-length dimension stays as separate runs (64k/128k need YaRN-scaled serving). This removes_ruler_avg.py, theruler-avgcommand, and theoutput.pyrenderer entirely — no benchmark-specific logic in the CLI, no parsing length out of task-name strings.
Other
- Bug: CWE
english_words.jsonpinned tomain(ruler_cwe.py:44), unlike the deliberately-pinned HotpotQA revision. It's the long-context vocab fallback used exactly at 64k/128k. Pin to a SHA. - Nit: Category.
Logic/TextualReasoningis a poor fit for retrieval/aggregation tasks. Move toLanguage/SemanticUnderstanding; thelong-contexttag already carries the capability axis. - Reproducibility: ship at least one example eval config (now small — one ruler task per length) and document the non-obvious prerequisites:
continue_final_message: true(without it the answer-prefix continuation silently breaks), YaRN for >32k, andtokenizer_pathmust match the target model (RULER sizes prompts to the model's tokenizer). The config that produced the run was dropped in4c1d1749andexamples/has none.
(Dockerfile / deps-in-image handled separately.)
…le fix) Three related cleanups from the scitix#11 review: local: → true BYO. The Paul Graham essay corpus is not redistributable (Apache-2.0 covers NVIDIA's scraper, not the essays) and must not ship in the wheel or repo. Replace the copy-from-package model: - LocalHandler.download() moves no bytes — no-op if the corpus is already staged, else raises LocalSourceUnavailable naming the expected path. Drops the _bundled_path/_data resolution entirely. - `sieval dataset download` catches that and prints the instructions as a non-fatal notice, so the fetchable url:/hf: sources still download and the command doesn't hard-fail on the BYO piece. - gen_paul_graham_essays.py takes --data-dir (default $SIEVAL_DATA_DIR) and writes straight to <data-dir>/ruler/PaulGrahamEssays.json.gz — the loader's read path — so no staging copy is needed. - Rewrite test_local.py for the no-op/raise semantics. nltk dependency. niah/vt essay tokenization imports nltk, but the ruler extra only declared tiktoken/wonderwords/numpy/scipy; check_dep_coverage passed only because nltk was reachable via ifeval, so a clean `pip install sieval[ruler]` broke at load. Relock adds only nltk's group membership — no version drift. Example fix. The nonthinking-withyarn-64k config ran enable_thinking:false on the model but enable_thinking:true in the dataset args, over-reserving the thinking token budget for a run that never thinks. Align the dataset arg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ethan-scitix
left a comment
There was a problem hiding this comment.
Verdict: Comment — port is faithful and reproduction checks out; a few consistency/onboarding items before merge.
Verified empirically: string_match_all/string_match_part, templates, and token budgets are byte-exact to NVIDIA/RULER at ab17b78; the vendored matcher reproduces the stored subtask scores exactly (cwe=98.54, qa_squad=76.8, fwe=97.53), and the per-length means average to ≈88, consistent with the reported 88.2.
Should fix before merge
Bug(consistency): the per-sample retry loops differ across siblings. Upstream RULER's niah/qa/vt/cwe all lack the floorelse(all can spin atused <= incremental). This port addedelse: breakto_niah.py:159,_vt.py:155,_cwe.py:84but not_qa.py:81— so three deviate from upstream and one matches it. Practically unreachable at 4k+, but pick one behavior for all four: either match upstream exactly (drop theelse), or keep the guard everywhere and annotate the deviation in code. Author's call, but it must be consistent.Nit(architecture):_ChatGenBase(ruler_0shot_gen.py:51) has a single subclass. Inlinepreprocess/infer/postprocessintoRulerZeroShotGenTaskand delete the base.Nit(encapsulation):ruler_0shot_gen.py:73,85readsself.model._kwargs/_model; use the publicself.model.meta()instead.Nit(robustness):sieval dataset download rulerswallowsLocalSourceUnavailableand exits 0 (cli/dataset/commands.py:190). Track missing requiredlocal:sources and print a clear final summary + exit non-zero, so a missing corpus isn't mistaken for success.Nit(onboarding): the BYO corpus step is only in the script docstring / error message, not in the config setup or docs, and the generator's deps (html2text,beautifulsoup4,certifi) are undeclared — so following the example config fails at load time, and running the script fails on import. Please: (1) document the full flow (install extra → generate corpus →downloadthe rest → run), (2) declare the generator deps (e.g. aruler-genoptional group), and (3) havedownloadprint the exact generate command rather than auto-scraping.
Nits
- Revert the
.pre-commit-config.yaml--maxkb=2000bump — the corpus is BYO/not committed, so it's vestigial and its comment (references bundling undersieval/datasets/_data/) is misleading. - ty overrides (
pyproject.toml): narrowsieval/datasets/ruler/**to the specific loader files (or install therulerextra in the type-check env); drop thescripts/exemption and declare its deps instead — exempting it hides that the script isn't runnable out of the box. wonderwords.random_word._get_words_from_text_file(_niah.py:224,_cwe.py:144): private API, faithful to upstream — keep, but add a comment noting the risk and cap the version (<3).- Trim examples to
nonthinking+thinking(flat, no subdir); fold the 64k/128k yarn variants into a comment.
PR body
- "Actual: 87.7" contradicts the table's 88.2 (89.1 − 0.9 = 88.2, −1.01%) — 88.2 is the real number.
- Summary says answer_prefix is appended to the user message "without a prefilled assistant turn," but the shipped nonthinking config and the reproduction runs use the assistant-prefill path (
continue_final_message: true). Align the description with what was validated.
…ged as truncated downloads The truncation check in url.py compared bytes *written to disk* against Content-Length. That breaks for Content-Encoding responses: when the server sends gzip, Content-Length is the compressed size while iter_bytes() yields the larger decompressed body, so written bytes always exceed Content-Length and the download is falsely rejected. Compare r.num_bytes_downloaded (raw on-the-wire bytes, before httpx decompresses) against Content-Length instead — the two are now measured on the same, compressed scale, so gzip-served files pass while genuinely truncated transfers still fail. Regression: SQuAD's train-v2.0.json is gzip-served; Content-Length=9551051 but the decoded body is ~42MB, which the old written-bytes check rejected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(datasets): add LocalHandler to stage local:<relpath> assets from sieval/datasets/_data/ into dest_root/<dataset_name>/ feat(datasets): enforce normalized package-relative paths and reject absolute / traversal inputs in _bundled_path() feat(datasets): align output filename behavior with URL downloader via shared url_path_basename fallback (download) test(datasets): cover scheme validation, traversal guards, copy/skip/force behavior, and is_downloaded() checks for local downloader Co-authored-by: Claude Sonnet 4.6 (Anthropic) noreply@anthropic.com
Extend the lazy-loading discovery in datasets/__init__ (and the stub sync) to scan subpackages, so a benchmark can live in its own directory. Vendor RULER's prompt templates and string_match metrics under community/ruler, and bundle the Paul Graham Essays haystack corpus (read via the local: scheme) with a one-shot regeneration script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the five parameterized RULER loaders covering all 13 task configs. Synthesis is aligned to upstream NVIDIA RULER: binary-search context sizing (niah/vt/cwe), VT's built-in 1-shot ICL, FWE's tokens_to_generate budget reservation, and essay-repeat on overflow. vt/cwe/fwe are fully synthetic (no download); niah/qa stage from bundled/url sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add 10 RULER tasks — chat and base/completion variants of niah/vt/cwe/ fwe/qa — over shared scoring (string_match_all/part) and endpoint mixins. Add `sieval leaderboard ruler-effective` to aggregate a multi-length sweep into per-length 13-task averages and the threshold-based effective length. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add single-length (chat) and QA-only (base/completion) example configs, plus a generated multi-length sweep and the gen_ruler_sweep.py tool that expands the 13 configs across length tiers with per-tier YARN factors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add --tokenizer-model to gen_ruler_sweep.py (default = --checkpoint) and write it into every dataset's args, so synthesis sizes prompts with the evaluated model's own tokenizer — matching upstream RULER, which sets the dataset tokenizer to the model path. Without this, prompts were measured with the gpt-4 tiktoken default and the length tiers drifted off-target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
curtis.ml.cmu.edu is no longer reachable. Replace the url: source with
hf:hotpotqa/hotpot_qa and rewrite _read_hotpotqa to call load_dataset()
with the HF distractor split. Context is now read from the HF schema
(context={'title': [...], 'sentences': [[...], ...]}) rather than the
JSON file's list-of-pairs format. Update tests to mock load_dataset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Update qa_hotpotqa subdir from 'ruler_qa' to 'hotpotqa'
- Align generated YAML configs to actual dataset locations:
- SQuAD: ${SIEVAL_DATA_DIR}/ruler_qa
- HotpotQA: ${SIEVAL_DATA_DIR}/hotpotqa
- Add SGLang deterministic inference flag (enable_deterministic_output)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Remove completion endpoint (base_gen) tasks - Supported endpoint now chat-only via ChatModel - Rationale: RULER evaluation focuses on instruction-following via chat templates Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Update _base.py: remove _BaseGenBase class - Gen script: remove endpoint parameter, only support chat - Update docstrings and tests - Simplify task class registry to remove endpoint branching Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add error handling in _read_hotpotqa: fallback from 'distractor' to 'fullwiki' config - Fix BuilderConfig 'distractor' not found error with graceful fallback - Regenerate qwen3-8b_4k_sglang.yaml with correct hotpotqa path Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Remove fullwiki fallback, keep distractor-only - Handle local vs remote dataset loading: try with config first, then without - Fixes 'BuilderConfig' errors when loading from cache or local files Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Import string_match_all and string_match_part from community.ruler.eval.constants - Unify recall and QA scoring: collect predictions+references in feedback, compute batch score in report - Align RecallFeedback with QaFeedback structure (prediction + references) - Ensures metrics match upstream RULER exactly across all task types Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add extract_task_order() to parse YAML config and extract task ordering - Update collect_sweep() signature to accept optional task_order parameter - Update summarize() to include task_order in output when provided - Add --config parameter to ruler-effective command - Enables output sorted by config definition order instead of alphabetical Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add collect_sweep_with_tasks() to group scores by task (not just aggregate) - Update summarize() to include 'per_task' section with individual task scores - Per-task results are ordered according to config task_order when available - Output structure: per_task[length]['tasks'][task_base_name] = score Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add per-task details section to text output
- Tasks are displayed in config order (if task_order available) or alphabetically
- Output format:
Task Details:
4k
ruler_niah_single_1: 92.50
ruler_niah_single_2: 89.30
...
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add enable_deterministic_inference: true to model overrides (server-side) - This is different from extra_body parameter (client-side) - SGLang server startup now receives this parameter via overrides - Fixes log showing enable_deterministic_inference=False Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…display - task_order contains full names with _Nk suffix (ruler_niah_single_1_4k) - tasks dict keys are base names without suffix (ruler_niah_single_1) - Strip suffix from task_order entries before matching against tasks dict - Fixes missing task details in terminal output Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- If --config not provided, automatically search for effective_config.yaml - Search in the first output directory (where results are stored) - This file is persisted by sieval run and contains the original config - Falls back to no task_order if file not found - Enables seamless task ordering without explicit config parameter Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…se names - task_order contains full names with _Nk suffix (ruler_niah_single_1_4k) - tasks_at_length keys are base names without suffix (ruler_niah_single_1) - Strip suffix from task_order_bases before matching in summarize() - Fixes issue where per_task results were always empty Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add support for multiple context lengths (8k, 64k, 128k) - Add base configuration for Qwen3 8B with SGLang - Update dependencies and Python version constraint to 3.12.* - Lock dependency versions for stability
…ontinuation
Align the RULER datasets and tasks with the upstream NVIDIA RULER
implementation (prepare.py + synthetic scripts) so the prompt ends with
the answer cue and the model continues from it, and fix latent bugs that
broke loading, scoring, and downloads.
Datasets (sieval/datasets/ruler):
- ruler_vt.py: fix the port — keyword-only signatures, binary-search
noise sizing, essay/noise haystacks, built-in 1-shot ICL; drop the
broken bare `from _common import` for the package-relative import
- ruler_niah.py: import _build_haystack/_NEEDLE from _common instead of a
duplicated local copy; add the missing `from nltk.tokenize import
sent_tokenize` that broke the essay path
- _common.py: become the single source of _build_haystack and the
haystack constants; drop the stray `self` parameter
- ruler_cwe.py: fix the broken os.path.dirname() call; load the optional
english_words.json from the staged data dir; replace bare `except`
- ruler_qa.py: switch tokenizer.encode() to text_to_tokens()
- ruler_{niah,vt,qa,cwe,fwe}.py: append answer_prefix to the template
before generation (mirrors prepare.py) so the loader can split it back
out; align every *DatasetSample TypedDict with the real rows.append
schema (index/input/outputs/length/answer_prefix; niah adds
token_position_answer); default the tokenizer to openai/cl100k_base so
loading is offline and portable
- ruler_cwe.py: register english_words.json as a `url:` source so it is
fetched by `sieval dataset download`
Tasks (sieval/tasks/ruler/_base.py):
- preprocess: split the prompt into a user turn (body) and an assistant
prefill turn (answer_prefix) so the model continues the cue; the prefill
toggle (continue_final_message/add_generation_prompt) is left to the run
config's extra_body, not hardcoded, to avoid clobbering it
- RulerRecallSample and the recall scoring mixin now read `outputs`
(previously `answer`, which never matched the dataset rows)
Community (sieval/community/ruler):
- add scripts/tokenizer.py with select_tokenizer (hf/openai backends)
- remove the obsolete scripts/template.py; trim datasets/eval constants
Config & meta:
- examples/qwen3-8b_4k_sglang.yaml: enable continue_final_message /
add_generation_prompt in the model extra_body
- sieval/meta/index.json: regenerate from the updated registries
Tests (tests/unit/{datasets,tasks}/ruler):
- rewrite the dataset and task unit tests for the new row schema and the
user+assistant message structure; use the offline tiktoken tokenizer;
fix stale imports (build_tokenizer) and signatures (_get_example). All
36 ruler unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nization * Use HuggingFace tokenizer (model-specific) instead of tiktoken for all RULER datasets (NIAH, VT, CWE, FWE, QA) to match actual token counting at inference time, fixing context-length overages with Qwen models * Account for Qwen3 thinking tags (<think>...</think>) overhead in dataset synthesis by pre-calculating token cost when enable_thinking=false * Add enable_thinking parameter to all dataset loaders to track extended thinking overhead during prompt sizing * Update gen_ruler_qwen3_8b_sglang.py to pass tokenizer_type='hf' and tokenizer_path (model path) instead of tokenizer_model * Add Qwen3-aware prompt preprocessing to inject thinking tags for extended reasoning even when enable_thinking=false (workaround for thinking framework) * Fix HotpotQA data loading: add more_context field from doc pool to support distractor sampling in QA generation Fixes context-length violations in RULER evaluation where dataset synthesis used tiktoken (generic) but inference used model-specific tokenizers with different token counts. Qwen3's thinking tags now properly accounted for. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Re-add the example config that was inadvertently removed during the Qwen3 ruler dataset-generation work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Experimental QA-only change on top of feat/ruler (re-run the Qwen3-8B repro before promoting, since it changes generated bytes): - HotpotQA document order: switch from first-seen insertion order to alphabetical `sorted(set())`, byte-identical to upstream qa.py so the document indexing and distractor selection match upstream. Removes the ordering divergence. - reference_impl.notes: drop the now-resolved HotpotQA-order divergence; the generation cap, essay concat order, and CWE <=32k scoping remain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ethan-scitix
left a comment
There was a problem hiding this comment.
Bug: qa_hotpotqa never reads the staged snapshot; silently falls back to a live online fetch. ruler.py:56 routes it to the base data dir, so _qa.py:171 calls load_dataset("<data_dir>", "distractor", …) — which raises ValueError: BuilderConfig 'distractor' not found (reproduced against the real staged layout), caught at _qa.py:172 → always drops to online load_dataset("hotpotqa/hotpot_qa", …). So the pinned copy download stages is dead, and eval fails under HF_HUB_OFFLINE=1 despite a completed download. Point the loader at <data_dir>/hotpotqa/hotpot_qa and keep online as an explicit last resort.
Nit: unrelated churn in leaderboard/commands.py:64-73. Loop→comprehension rewrite of report(), no RULER connection — leftover from the removed ruler-avg command. Please restore origin/main's version so the PR touches no unrelated CLI file.
Nit: two of the four external sources are downloaded but never consumed. Besides HotpotQA above, english_words.json is loaded into randle_words (_cwe.py:42-45) but never sampled — the CWE cap keeps num_words ≤ vocab. This is the documented ≤32k stable scope, so it's fine; just add one line to the source docstring so nobody assumes it's load-bearing.
Nit: _read_hotpotqa builds a dict it only uses as a set (_qa.py:180-189 — values discarded by sorted(set(...))). Use a set comprehension.
Housekeeping: rebase onto main to drop the closed-#9 downloader commits (branch is also behind #30/#34/#35/#37). ruler-gen generator deps stay undeclared — a conscious call, but running the script still ImportErrors with no install hint; a one-line pip install … in the usage/error path would close it.
qa_hotpotqa was routed to the base data dir, so _read_hotpotqa called
load_dataset("<data_dir>", "distractor", ...) — the base dir is not a
distractor-config dataset, so it always raised and silently fell back to
an online fetch. The pinned staged download was dead code, and eval failed
under HF_HUB_OFFLINE=1 despite a completed download.
Point the loader at the HF mirror's real landing spot <data_dir>/hotpotqa/
hotpot_qa (snapshot_download -> dest_root / repo_id), keeping the online
fetch as an explicit last resort. Extract _HOTPOTQA_REPO_ID as a single
source of truth shared by the loader path, the online fallback, and the
@sieval_dataset source so the download target and read path can't drift.
Add regression tests covering staged-copy-first reads and the
staged-absent online fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsion - commands.py: expand model-name resolution into an explicit loop with typed RunInfo list for readability - _qa.py: fold HotpotQA doc collection into a set comprehension - ruler.py: note english_words.json is staged for parity but not load-bearing in the <=32k stable scope Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Experimental changes layered on feat/ruler, split out per review (the Qwen3-8B repro should be re-run on this branch before promoting, since both change generated bytes): - HotpotQA document order: switch from first-seen insertion order to alphabetical sorted(set()), byte-identical to upstream qa.py. Removes the ordering divergence entirely (distractor selection now matches upstream). - Per-subtask generation cap: stamp each sample with `gen_budget` (tokens_to_generate for its RULER task, incl. any thinking overhead) and pass it as max_tokens in infer(), matching upstream's per-task cap (128/30/120/50/32). Replaces the implicit context-window bound that only held when serving context == max_seq_length. - reference_impl.notes: drop the now-resolved generation-cap and HotpotQA-order divergences; only the essay concat order and CWE <=32k scoping remain. - Tests: gen_budget stamping (_stamp + fwe schema) and the infer() max_tokens cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RULER prompt fitting counted only raw content + generation budget, never the inference-time message-template overhead (role markers, prefilled <think></think> block). At long contexts this let wrapped prompts overflow max_seq_length, producing failed samples (e.g. 128k runs). Introduce a two-function split in _shared.py with no double-counting: - calculate_prompt_tokens(): input side only — wraps the prompt in the real qwen3 template (via community template.py) and counts tokens using the RULER text_to_tokens interface. No think_budget. - tokens_to_generate(): output side (max_tokens) — includes think_budget and the generated <think> tag overhead, since thinking is generated. The only possibly-overlapping token, the <think> tag, is counted exactly once: prefilled in the prompt for non-thinking (input side), generated for thinking (output side). Thread enable_thinking/model_name through all five subtask fitters and per-sample length checks (_niah/_qa/_cwe/_fwe/_vt); VT keeps the ICL fragment as a raw count to avoid double-wrapping. FWE reserves template overhead in input_max_len and raises a clear error if think_budget swallows the context. Fix a SyntaxError (missing comma) in community/ruler/datasets/template.py. Tests: 64 pass; end-to-end FWE at 4k/32k in both thinking modes stays within max_seq_length with gen_budget correctly including think_budget. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flow)
get_template looked up lowercase "qwen3-nonthinking"/"qwen3-thinking" but
template.py registers them capitalized ("Qwen3-nonthinking"). The mismatch
silently fell back to the base template ("{task_template}"), so
calculate_prompt_tokens counted ZERO message-template overhead (~13 tokens:
role markers + prefilled <think></think> block). Prompts were sized that
much too large and overran max_seq_length by a few tokens at inference
(e.g. non-thinking 128k: 130947 input + 128 = 131075 > 131072).
- Resolve template keys case-insensitively; fail loud (KeyError) if a Qwen3
template is genuinely missing instead of silently degrading to base.
- Restore the Qwen3-thinking template (had lost its /think marker and
assistant turn).
With the fix, sizing matches the model's real chat_template within 1 token
(conservative). Adds regression tests: non-zero qwen3 overhead,
case-insensitive resolution, and fail-loud on a missing key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oken
Match NVIDIA/RULER prepare.py + niah.py (commit ab17b78): reserve
model_template_token = len(text_to_tokens(RAW template)), where the template
string still contains the literal {task_template} placeholder. Upstream does
`max_seq_length -= model_template_token` then fits with `<=`; folding the
reserve into calculate_prompt_tokens (content_tokens + model_template_token)
is algebraically identical.
Previously we tokenized the template with the real content substituted in,
reserving the *exact* overhead with zero slack. That let a sample fill the
context to exactly max_seq_length (input + completion == 131072), which the
serving engine rejects as exceeding the limit. Counting the unformatted
template reserves the placeholder's tokens too (~4-5) — they are replaced by
real content at inference and never emitted, giving the same headroom upstream
relies on. Verified: sizing now exceeds the model's real chat_template input
count by ~5 tokens (conservative), and FWE at 32k stays within budget in both
thinking modes.
Tests updated: base/unknown model reserves the "{task_template}" token count
(upstream-consistent); qwen3 thinking template distinguished by absence of a
prefilled </think> block rather than a /think marker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Qwen3 reasons by default when the assistant turn is opened without a prefilled <think></think> block, so the explicit /think soft-switch is redundant. The thinking template now just opens the assistant turn; the non-thinking template still prefills the empty block to suppress reasoning. get_template distinguishes the two by presence/absence of the prefilled block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s once Addresses two review findings: Structure:P0 — community/ vendor fidelity. The Qwen3-nonthinking/Qwen3-thinking entries do not exist in upstream RULER's template.py at the pinned SHA, so having them in the vendored community/ file polluted its provenance (and a re-vendor would silently drop them). Move the two Qwen3 templates into a sieval-owned _QWEN3_TEMPLATES dict in datasets/ruler/_shared.py and restore community/ruler/datasets/template.py to the upstream-verbatim key set. get_template now resolves Qwen3 from the local dict; other models still use the vendored Templates (case-insensitive, base fallback). Nit:P1 — compute sample-invariant work once. Replace calculate_prompt_tokens (which recomputed the template reserve on every call, inside the fitting loop) with model_template_token(): the upstream `len(text_to_tokens(raw template))` reserve, computed once per dataset build and threaded into each loader's fitter and per-sample length as `len(text_to_tokens(content)) + template_token`. Behavior unchanged: sizing still equals content + upstream model_template_token (+ gen budget), just computed once. 65 tests pass; ty/ruff clean; FWE 32k in both thinking modes stays within budget. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adapt tokens_to_generate() to support Qwen3's extended thinking by differentiating budget allocation between dataset generation and inference based on context_length: - Small contexts (4k, 8k): Skip think_budget during dataset generation (reuse native context window), add it during inference - Large contexts (32k, 128k): Reserve think_budget during dataset generation, don't readjust during inference Diverges from upstream RULER (@ab17b78) which uses single tokens_to_generate. This ensures samples fit target contexts while allocating sufficient max_tokens for thinking. Per Qwen3 technical report (think_budget=8192). - Add context_length and for_dataset params to tokens_to_generate() - Extend RulerDatasetSample with think_budget and enable_thinking fields - Adjust ruler.py _stamp() to preserve budget info in samples - Update ruler_0shot_gen.infer() to restore think_budget for small contexts - Forward context_length and for_dataset in all subtask loaders - Add 7 tests covering dataset/inference budget splits (48 tests total) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sync 7 upstream benchmark additions that were missing from the ruler branch (none touch ruler code): arc-easy/challenge (scitix#36), browsecomp (scitix#40), c-eval (scitix#15), hendrycks_math (scitix#31), mmlu k-shot clp (scitix#33), simpleqa-verified (scitix#38, scitix#39). Conflicts were limited to generated/lock files and resolved by regeneration: - pyproject.toml: auto-merged (math-verify pinned to [antlr4-11-0]==0.8.0 upstream) - pdm.lock: relocked with -G :all (re-includes ruler group: wonderwords, tiktoken) - sieval/{datasets,tasks}/__init__.pyi: regenerated via sync_package_stubs.py - sieval/meta/index.json: regenerated via sync_meta_index.py (30 datasets, 36 tasks) ruler source and the think_budget work are unchanged; ruler tests pass (72). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shorten docstrings and comments to meet ruff's 88-char line limit: - Condense param descriptions in tokens_to_generate - Split long comment lines in infer() and dataset generation - Shorten test docstrings All 48 RULER unit tests pass. Pre-commit checks also pass. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The upstream merge (e85dc99) re-resolved the loose `ty>=0.0.5` spec and bumped ty 0.0.20 -> 0.0.59, whose stricter checks flag 150 pre-existing diagnostics in code byte-identical to upstream/main (green on 0.0.20). Restore the exact ty 0.0.20 lock block from upstream/main for parity; pyproject is untouched, no other package versions drift. Also add @_needs_ruler_deps to the tokens_to_generate tests: without the marker they raised NameError instead of skipping when the ruler deps group is absent (CI light env), since the symbol is imported under the `if _ruler_deps:` guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_ruler_shared.py does a module-level `import tiktoken` behind try/except to gate the ruler-deps tests. In the CI light env tiktoken is not installed, so ty reports unresolved-import for it. The ruler ty override already ignores this rule for the other module-level ruler-dep importers (_niah.py, _cwe.py, test_ruler.py); add test_ruler_shared.py to the same include list. (Function-local lazy imports in _vt/_shared/ _fwe are not flagged, so they need no entry.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove 32k context size distinction — only 128k contexts need special handling for Qwen3 extended thinking budget allocation. Changes: - tokens_to_generate(): update condition from `not in (32768, 131072)` to `!= 131072` (128k only) - RulerZeroShotGenTask.infer(): align logic and update all comments - Update docstring examples to reflect 128k-only handling Verified: tests/unit/tasks/test_ruler_0shot_gen.py (22 passed) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit 59ec90d (mislabeled 'apply ruff formatting') overwrote pyproject.toml with pdm-init defaults, which broke CI preflight with 'No module named sieval': - restore [build-system] (pdm-backend) and distribution = true so the project is installed as a package again - restore description, authors, Apache-2.0 license - restore requires-python '<3.15,>=3.12' (==3.12.* broke the 3.13 matrix) - drop stray version = '0.1.0' (conflicts with dynamic scm versioning) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit 59ec90d also narrowed pdm.lock to match the corrupted pyproject: requires_python became ==3.12.* and the py3.13-only pyyaml-ft package was dropped, so the lock hash no longer matched the restored pyproject and CI's --frozen-lockfile check failed. Restore the lock to its pre-corruption state (3.12+3.13 targets, pyyaml-ft present); hash now matches pyproject again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/gen_paul_graham_essays.py imports html2text, beautifulsoup4, and certifi at module level, but no installable dependency group provided them — the documented corpus-regeneration path (the only way to obtain the Paul Graham essay haystack, a local: BYO source) would ImportError on a clean env. Add a dedicated ruler-gen optional group (kept out of ruler since it's not needed to run the benchmark) so the script is runnable via `pdm install -G ruler-gen`. Update the ty override comment accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge the 'all'/list aggregate path and the single-subtask path into one dispatch loop (a single subtask is a one-element target list). Removes the ~30-line recursive self.load() kwarg re-listing and keeps the staging-path rule and budget stamping identical across every entry point. Also drop 6 vestigial load() params (num_needle_k/v/q, type_haystack, type_needle_k/v): NIAH config is fixed per subtask by _NIAH_SUBTASK_KWARGS and no subtask reads the top-level values — dead knobs whose only reader was the removed recursion. **kwargs still absorbs any stray value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dea9270 narrowed the think_budget reservation to 128k contexts only, but two dataset-gen tests still asserted the pre-change behavior at 32k (and a third had a stale docstring). They never ran in CI (gated on ruler deps, which the light CI install omits), so the drift went unnoticed. Update the 32k tests to assert small-context behavior (no think_budget reserved at gen) and fix the inference-test docstring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
synthetic.yaml was never parsed — the subtask names and NIAH config it lists are transcribed into _ALL_SUBTASKS / _NIAH_SUBTASK_KWARGS / TASKS, and nothing loads the file. Remove it (staged-but-not-load-bearing, drift risk) and repoint the two comments that cited it (ruler.py docstring, _niah.py) at the upstream NVIDIA/RULER synthetic.yaml @ab17b78 they were transcribed from. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both example headers were stale (single-16K/single-model, wrong GPU, obsolete budget note). Rewrite them to match the actual 4K-128K sweep with native/YaRN serving configs, and correct the think-budget coupling note (datasets now set think_budget: 8192). The example bodies gained per-length datasets/tasks and a YaRN model; thinking.yaml documents why 16K/32K run under YaRN (Qwen3 technical report alignment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BYO essay generator set converter.escape_all and .reference_links, which no longer exist on modern html2text (escape_all was renamed escape_snob; reference_links was removed — links are inline by default). Both were silent no-ops, so the intended markdown escaping never applied. Switch to escape_snob, drop the dead reference_links line, and upper-bound html2text (<2026) so the converter API can't drift out from under the script again. Surfaced as ty unresolved-attribute once html2text became installable via the ruler-gen group (CI's light install omits it, so CI never flagged it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ethan-scitix
left a comment
There was a problem hiding this comment.
To address
-
Question:Thinking long-context reproduction is unverified. In the real runs, think-32k-YaRN collapsed (overall 6.89, everything 0 except fwe) and think 64k/128k have no report. This is not a port bug — the same code gives non-think-32k=92.3 and think-4k/8k are fine, so it's the 32k+YaRN serving config. But the "reproduces published recall" claim inreference_impl.notes(sieval/tasks/ruler_0shot_gen.py:62) is currently only backed by non-think + think-≤8k. Please re-run think ≥32k to confirm, or scope thestatus="stable"/ repro claim to the validated configs and mark thinking long-context experimental. -
Nit:The divergence notes read as exhaustive but omit 4 (all score-neutral, confirmed by the runs) — add to the notes or align to upstream:estimated_maxbounding formula differs from upstream (sieval/datasets/ruler/_niah.py:210/_vt.py:246/_cwe.py:123/_qa.py:133) → different binary-search probe trajectory → different global-RNG consumption → samples not byte-reproducible vs upstream. Prefer aligning to upstream (max_seq_length -= model_template_tokenup front, use the raw token count).- HotpotQA is sourced from the HF mirror, not upstream's derived json; questions are taken in native order and sliced to the first N, so per-index selection may differ (
sieval/datasets/ruler/_qa.py:182). The notes only cover doc ordering. - essay
escape_snob=True(scripts/gen_paul_graham_essays.py:131) escapes markdown, but upstream'sescape_allis a no-op (unescaped — confirmed in the staged corpus) → diverges from upstream's actual output. Either revert to unescaped, or record it as a divergence and note the repro corpus must be regenerated. - VT ICL fragment is sized against 500, not 500−T (
sieval/datasets/ruler/_vt.py:128, currently only in a helper docstring).
-
Nit:CWE drops sharply in thinking mode under the cap (non-think 8k 98.3 → think 8k 69.1); predictions are verbose lists truncated mid-answer. Upstream's cap=120 assumes a terse answer — worth noting its effect on thinking-mode CWE in the notes. -
Nit:Inconsistent qwen3 detection:thinking_prefilluses substringin(sieval/datasets/ruler/_shared.py:164) whileget_template/tokens_to_generateuse.startswith(:224/:112). For non-standard model ids, packing and inference can disagree on the<think>block — align them.
Type
Summary
Adds the RULER long-context benchmark (13 configs across 4 categories) as
synthetic datasets + thin generative tasks, adapted from NVIDIA RULER
full prompt at load time; haystack corpus (Paul Graham Essays) is generated
once and bundled in-tree, staged via the
local:scheme.string_match_all/string_match_part. VT/CWE arekshot(n_shot=1) becauseupstream bakes one in-context demonstration into the prompt; the rest are 0-shot.
to upstream with attribution.
is revision-pinned for reproducibility.
Reference: https://github.com/NVIDIA/RULER
Related Issues
Test Plan
Automated
ruff check && ruff format --check)ty check; ty pinned to 0.0.20)rulerextra):pdm run pytest tests/unit -m "not stress" --cov→ 2433 passed, 39 skipped,98.44% coverage (coverage scope is
sieval/core). RULER tests requiring therulerextra skip here;pdm install -G ruler && pdm run pytestruns the fullsuite (ruler tests green locally).
Manual
Reproduces RULER aligned with NVIDIA RULER. Compared against the
RULER score in the Qwen3 technical report (arXiv:2505.09388) — note Qwen does
not publish the exact prompt/decoding/answer-extraction config, so the target
is "land close to the report" rather than byte-exact reproduction.
Checklist
Required (all PRs)
type(scope): description)AI-Generated Code - <model> (<provider>)markercore/If: New or Modified Benchmark
sieval dataset downloadpath; local/url/hf sources resolve)__init__.pyIf: community/ Changes
If: New Dependency
ruler: tiktoken, wonderwords, numpy, scipy, nltk, transformers)