Skip to content

feat(ruler): add RULER long-context benchmark (datasets + tasks)#11

Open
Mea1Ma wants to merge 106 commits into
scitix:mainfrom
Mea1Ma:feat/ruler
Open

feat(ruler): add RULER long-context benchmark (datasets + tasks)#11
Mea1Ma wants to merge 106 commits into
scitix:mainfrom
Mea1Ma:feat/ruler

Conversation

@Mea1Ma

@Mea1Ma Mea1Ma commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Note: This PR is stacked on #9 (add local: source handler and fix url: gzip truncation check
). Those commits appear in this diff too because GitHub can't target a fork branch as the base. Please review the downloader changes in #9; once it merges I'll rebase this onto main to drop them. The RULER work here depends on the local: handler (the Paul Graham haystack is bundled in-tree and staged via local:).

Type

  • feature — new benchmark, task, or capability

Summary

Adds the RULER long-context benchmark (13 configs across 4 categories) as
synthetic datasets + thin generative tasks, adapted from NVIDIA RULER

  • datasets/ruler/ — 5 synthetic loaders (niah/qa/vt/cwe/fwe) that build the
    full prompt at load time; haystack corpus (Paul Graham Essays) is generated
    once and bundled in-tree, staged via the local: scheme.
  • tasks/ruler/ — thin chat tasks that send the prompt and score by RULER's
    string_match_all/string_match_part. VT/CWE are kshot (n_shot=1) because
    upstream bakes one in-context demonstration into the prompt; the rest are 0-shot.
  • community/ruler/ — vendored RULER constants/tokenizer/scoring, kept close
    to upstream with attribution.
  • Decoding params stay in user config (tasks never inject them); HotpotQA source
    is revision-pinned for reproducibility.

Reference: https://github.com/NVIDIA/RULER

Related Issues

Test Plan

CI note: ty is pinned to 0.0.20 in the lock for parity with main
(an upstream merge had re-resolved the loose ty>=0.0.5 spec up to 0.0.59,
whose stricter checks flag ~150 pre-existing diagnostics repo-wide). RULER's
deps (tiktoken/wonderwords/nltk/scipy/transformers) are absent from
the light CI env, so the ruler tests are gated behind @_needs_ruler_deps
and skip there.

Automated

  • Lint/format clean (ruff check && ruff format --check)
  • Type check clean (ty check; ty pinned to 0.0.20)
  • Unit tests pass — CI light env (no ruler extra):
    pdm run pytest tests/unit -m "not stress" --cov2433 passed, 39 skipped,
    98.44% coverage
    (coverage scope is sieval/core). RULER tests requiring the
    ruler extra skip here; pdm install -G ruler && pdm run pytest runs the full
    suite (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.

  • Model tested: Qwen3-8B, both non-thinking and thinking modes (matches the report's settings)
  • Coverage: full 4k–128k length sweep (4k/8k/16k/32k/64k/128k), 13-task avg, for both modes
  • Reference (Qwen3 tech report, RULER avg over 4k–128k): 89.1 (non-thinking) / 84.4 (thinking)
  • Actual (this implementation): 88.12 (non-thinking) / 86.86 (thinking)
  • Diff: -0.98 (-1.10%) non-thinking, +2.46 (+2.91%) thinking — both within target < 3%
Model Mode Expected Actual Diff
Qwen3-8B Non-thinking 89.1 88.12 -0.98 (-1.10%)
Qwen3-8B Thinking 84.4 86.86 +2.46 (+2.91%)

Checklist

Required (all PRs)

  • PR title follows conventional format (type(scope): description)
  • No internal paths, credentials, or personal info in committed files
  • AI-generated code has AI-Generated Code - <model> (<provider>) marker
  • No new upper-layer dependencies added to core/
  • Deleted code verified — no remaining call sites depend on it

If: New or Modified Benchmark

  • Reference paper/repo linked in Summary
  • Score comparison table included (model, expected, actual, diff)
  • Dataset loading tested (sieval dataset download path; local/url/hf sources resolve)
  • Task registered in package-level __init__.py

If: community/ Changes

  • Upstream diff documented (vendored from NVIDIA RULER, attribution in headers)
  • License attribution preserved (Apache-2.0 headers restored)

If: New Dependency

  • Added to correct PDM dependency group (ruler: tiktoken, wonderwords, numpy, scipy, nltk, transformers)
  • Justified in Summary (tiktoken/wonderwords for prompt synthesis, scipy/numpy for sampling, nltk for sentence tokenization, transformers for the HF/Qwen3 tokenizer path)

@ethan-scitix ethan-scitix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 .gz is just one local scrape frozen. Host one copy (HF @sha, or any immutable/public OSS object via url:) and keep scripts/gen_paul_graham_essays.py as 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 the url: 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.
  • nltk missing from the ruler extra. pyproject.toml:39 lists tiktoken/wonderwords/numpy/scipy but not nltk, which the default niah/vt essay path imports (_common.py:71, ruler_niah.py:285, ruler_vt.py:315). check_dep_coverage passes only because nltk is in ifeval; a clean pip install sieval[ruler] breaks at load. Add nltk to the ruler group.

Structure (the bigger one)

  • Collapse to 1 dataset + 1 task, mirroring MMLU. The 5 datasets / 5 empty task classes + the new leaderboard ruler-avg command are all symptoms of the same gap. MMLU already models 57 subtasks as one dataset with a Subject column and aggregates per-group in report() (mmlu_0shot_gen.py:92,102). Do the same: one RulerDataset emitting a subtask column, one RulerTask whose report() groups by subtask (right matcher per group — string_match_all vs string_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, the ruler-avg command, and the output.py renderer entirely — no benchmark-specific logic in the CLI, no parsing length out of task-name strings.

Other

  • Bug: CWE english_words.json pinned to main (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/TextualReasoning is a poor fit for retrieval/aggregation tasks. Move to Language/SemanticUnderstanding; the long-context tag 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, and tokenizer_path must match the target model (RULER sizes prompts to the model's tokenizer). The config that produced the run was dropped in 4c1d1749 and examples/ has none.

(Dockerfile / deps-in-image handled separately.)

Mea1Ma added a commit to Mea1Ma/sieval that referenced this pull request Jul 2, 2026
…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 ethan-scitix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 floor else (all can spin at used <= incremental). This port added else: break to _niah.py:159, _vt.py:155, _cwe.py:84 but 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 the else), 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. Inline preprocess/infer/postprocess into RulerZeroShotGenTask and delete the base.
  • Nit (encapsulation): ruler_0shot_gen.py:73,85 reads self.model._kwargs/_model; use the public self.model.meta() instead.
  • Nit (robustness): sieval dataset download ruler swallows LocalSourceUnavailable and exits 0 (cli/dataset/commands.py:190). Track missing required local: 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 → download the rest → run), (2) declare the generator deps (e.g. a ruler-gen optional group), and (3) have download print the exact generate command rather than auto-scraping.

Nits

  • Revert the .pre-commit-config.yaml --maxkb=2000 bump — the corpus is BYO/not committed, so it's vestigial and its comment (references bundling under sieval/datasets/_data/) is misleading.
  • ty overrides (pyproject.toml): narrow sieval/datasets/ruler/** to the specific loader files (or install the ruler extra in the type-check env); drop the scripts/ 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.

Mea1Ma and others added 25 commits July 6, 2026 18:17
…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 ethan-scitix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Mea1Ma and others added 16 commits July 10, 2026 13:15
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>
Mea1Ma and others added 9 commits July 23, 2026 14:59
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 ethan-scitix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

To address

  1. 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 in reference_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 the status="stable" / repro claim to the validated configs and mark thinking long-context experimental.

  2. 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_max bounding 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_token up 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's escape_all is 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).
  3. 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.

  4. Nit: Inconsistent qwen3 detection: thinking_prefill uses substring in (sieval/datasets/ruler/_shared.py:164) while get_template / tokens_to_generate use .startswith (:224 / :112). For non-standard model ids, packing and inference can disagree on the <think> block — align them.

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.

5 participants