Skip to content

feat(fireworks): fold ECHO env loss into the client loss path (single pass)#688

Draft
jeffreysijuntan wants to merge 16 commits into
terminal-rlfrom
echo-client-loss
Draft

feat(fireworks): fold ECHO env loss into the client loss path (single pass)#688
jeffreysijuntan wants to merge 16 commits into
terminal-rlfrom
echo-client-loss

Conversation

@jeffreysijuntan

Copy link
Copy Markdown
Contributor

What

ECHO's environment-prediction loss is currently applied on the Fireworks backend as a second, gradient-accumulated cross_entropy forward_backward on top of the builtin GRPO kernel pass. The managed builtin kernel can only express the policy loss, so the aux term needed its own pass (extra forward + backward over the same rollout tokens).

This PR folds ECHO into a single pass via the cookbook's client loss path. When an auxiliary loss is active, forward_backward_from_trajectory_groups now runs one forward_backward_custom whose closure computes the policy loss (training.utils.rl.losses.build_loss_fn) plus each aux term, from the single forward's per-token logprobs. Non-ECHO runs keep the fast server-side builtin kernel path. Tinker is unchanged (its cookbook has no client-side policy closure; ECHO isn't wired on Tinker in the cookbooks).

Why it's equivalent

  • Env term: reuses the exact same per-token weights the separate pass built (build_aux_ce_datum with scale = coef/(n_seq·|O_i|)). Adding Σ_t weights_t·(−logp_t) inside the closure vs. a separate accumulated cross_entropy backward yields the same gradient.
  • Policy term: now computed by the cookbook's client closure (make_grpo_loss_fnrun_loss_loop) instead of the server builtin kernel. The cookbook asserts these are in-lockstep (the client path is its documented always-correct fallback), so any difference is at most fp-level. Advantages are folded and optim_step keeps GradAccNormalization.NONE while aux losses are active, exactly as before — neither term is rescaled.

Cost

ECHO drops from ~2 fwd + 2 bwd to ~2 fwd + 1 bwd per step (forward_backward_custom = one logprobs forward + one linear backward). One fewer backward, no extra rollouts.

Changes

  • rllm/trainer/fireworks/fireworks_policy_trainer.py
    • new _forward_backward_client_with_aux (client-path fused pass)
    • new module-level _fold_aux_into_loss (wraps the policy closure to add aux terms)
    • branch in forward_backward_from_trajectory_groups: aux active → client path, else builtin kernel (unchanged)
    • removed now-dead _build_aux_datums; updated optim_step comment
  • tests/unified_trainer/test_aux_loss.py: fold reproduces the paper's L_env; _fold_aux_into_loss adds the term with the correct gradient (d/dlogp = −weights) and skips obs-free datums

Notes

  • Faithful port: the managed env mask is mask==0, which includes initial-prompt tokens (pre-existing behavior; verl's ECHO excludes them via the response mask). Preserved here to keep equivalence — flagging in case we want to align them separately.
  • Verification: py_compile ✓, ruff ✓, trainer + backend import ✓, 19/19 test_aux_loss.py pass.

🤖 Generated with Claude Code

signalrush and others added 16 commits June 20, 2026 01:22
…uation mangling (#656)

* fix(sandbox): add [environment].replay_dockerfile toggle + fix RUN-continuation mangling

Non-docker backends (modal/daytona) replay a task's Dockerfile RUN steps on top
of the booted image. Two defects (#655):

1. Multi-line `RUN ... \` steps were rejoined with "\n" instead of a space,
   producing invalid shell (observed: `bash: syntax error near '&&'`).
2. The replay assumes the configured image is a *base*. Terminal-bench / Harbor
   tasks boot a *fully-built* image, so replaying the RUN steps double-applies
   the build (`git clone ... already exists`, missing COPY'd files, etc.).

Fixes:
- `_dockerfile_run_commands` joins `\`-continuations with a space so multi-line
  RUN steps stay valid when re-exec'd via `bash -c`.
- New `[environment].replay_dockerfile` toggle. Default `true` preserves
  SWE-bench base-image behavior; `false` skips replay for prebuilt task images.
  Honored across the resolution cold path, daytona + modal snapshot builds, and
  the snapshot env-key fingerprint.
- Tests for the continuation join, COPY skipping, and the toggle (incl. the
  task.toml -> metadata path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: apply ruff-format to satisfy pre-commit

Collapse multi-line expressions onto single lines per ruff-format
(line-length=200), fixing the failing pre-commit check on PR #656.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
save_config() rebuilt config.json from scratch, writing only
provider/model/api_keys/base_url. Any other key was dropped — so
`rllm model swap`/`setup` silently wiped the `ui_api_key` written by
`rllm login`, logging the user out of the rLLM UI. Merge into the
existing file instead (mirroring save_ui_config). Adds a regression test.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#658)

* feat(gateway): cumulative token mode for the in-process (Tinker) path

`cumulative_token_mode` (drift-free multi-turn token forwarding: turn 2+ is
rewritten to a pre-tokenized prompt built by renderers.bridge_to_next_turn,
avoiding decode→re-encode drift) previously only worked for the HTTP-proxy
(vLLM/verl) path — the cumulative handlers always routed to an HTTP worker's
/v1/completions, so backends that run in-process via `local_handler` (Tinker)
fell back to re-rendering + re-tokenizing the full conversation every turn.

This wires the same feature through the local_handler path, reusing the
existing backend-agnostic TokenAccumulator + Prime Intellect `renderers`
bridge:

- tinker_adapter: the handler now detects a pre-tokenized `prompt` (list[int])
  and samples straight from it via the engine's existing
  get_token_output_from_token_input + assemble_model_output, returning a
  completions-style body (prompt_token_ids + choices[].token_ids) — the shape
  the gateway's cumulative handler already extracts token IDs from. The chat
  (messages) path is unchanged.
- proxy: _handle_cumulative_non_streaming routes to local_handler when present
  (no HTTP worker); added _handle_cumulative_streaming_local to synthesize an
  SSE stream from the single in-process completion (mirrors
  _handle_streaming_local), with the same token ingest.

Net effect: Tinker rollouts get the same prefix-extension guarantee verl has —
turn N's prompt tokens are byte-for-byte prior turns' prompt+completion tokens,
so the sequence the optimizer trains on matches what was generated.

Tests (no Tinker service / model required):
- tests/test_tinker_adapter_cumulative.py — token-prompt path samples from
  tokens; chat path unchanged; non-int prompt falls through.
- rllm-model-gateway/tests/unit/test_cumulative_token_mode_local.py — proxy
  local cumulative non-streaming + streaming ingest + chat translation.
Existing cumulative/accumulator suites (28) still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(gateway): wrap long SSE chunk lines to satisfy ruff E501

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cookbooks): add swe-rl recipe (rllm-swesmith → SWE-bench Verified)

End-to-end SWE-RL cookbook that pairs rLLM's native `rllm-swesmith`
training set with `harbor:swebench-verified` for eval, driving the
in-tree `mini-swe-agent` harness inside per-task sandboxes. Default
model is Qwen/Qwen3.5-9B + LoRA-32, GRPO + async + compact filtering,
64 parallel Daytona sandboxes.

No custom AgentFlow or evaluator: the harness owns the action loop,
each task's `tests/test.sh` is the verifier (pytest for swesmith, the
official SWE-bench harness for Verified), and the gateway captures
trajectories transparently.

Files: prepare_data.py (dataset pull), train.py + train_{tinker,verl}.sh
(recipes), test.py (catalog + harness smoke tests), README.md, pyproject.toml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cookbooks/swe-rl): use rllm.rollout.* keys; drop stale gateway overrides

The unified trainer config exposes sampling params at
`rllm.rollout.{train,val}.{temperature,top_p}`. The `sampling.*` paths
copied from `examples/harbor_swe/train_harbor.sh` predate the unified
config and break Hydra struct validation. Same template also passed
`rllm.gateway.public_url` / `rllm.gateway.sampling_params_priority`,
neither of which exist in the current schema — drop them.

Verified by resolving the config with --cfg job: no validation errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(swe-rl): run on rLLM-native SandboxedAgentFlow path; unblock async training

Switch the cookbook off the remote Harbor runtime onto rLLM's own
SandboxedAgentFlow path (AgentFlowEngine) and fix the bugs that surfaced
once real rollouts ran end-to-end on Modal sandboxes.

train.py / train_*.sh:
- Pass MiniSweAgentHarness as agent_flow so AgentTrainer auto-wires
  SandboxTaskHooks + per-task verifiers via AgentFlowEngine (not the
  remote_runtime/RemoteAgentFlowEngine path). Sandbox backend selected by
  SWE_SANDBOX_BACKEND (default modal).
- Load the val split as "default" (Harbor-pulled name), not "test".
- Async training requires train_batch_size=1 and raise_on_error=false; set
  both. Effective batch is mini_batch_size(16) groups x group_size(8).
- Add SWE_VAL_MAX to cap the 500-task SWE-bench-Verified val set.
- Drop stale rllm.remote_runtime.* overrides; doc the sandbox backends.

rllm/data/utils.py:
- task_from_row now roots the Task at the row's task_path and merges
  task.toml/Dockerfile metadata, so per-task verifier + image resolution
  work on the training path (fixes "No verifier configured").

rllm/gateway/tinker_adapter.py:
- Translate TerminationEvent into an OpenAI-standard 400
  context_length_exceeded instead of a 500. litellm maps it to a
  non-retryable ContextWindowExceededError, so an over-length in-sandbox
  agent stops immediately instead of retrying until the run-timeout
  SIGKILL — which was stalling group completion and wedging async steps.

rllm/harnesses/mini_swe_agent.py:
- Retry the in-sandbox uv install (Modal->GitHub egress resets the
  connection intermittently and aborted the install under set -e).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gateway): cumulative token mode for the in-process (Tinker) path

`cumulative_token_mode` (drift-free multi-turn token forwarding: turn 2+ is
rewritten to a pre-tokenized prompt built by renderers.bridge_to_next_turn,
avoiding decode→re-encode drift) previously only worked for the HTTP-proxy
(vLLM/verl) path — the cumulative handlers always routed to an HTTP worker's
/v1/completions, so backends that run in-process via `local_handler` (Tinker)
fell back to re-rendering + re-tokenizing the full conversation every turn.

This wires the same feature through the local_handler path, reusing the
existing backend-agnostic TokenAccumulator + Prime Intellect `renderers`
bridge:

- tinker_adapter: the handler now detects a pre-tokenized `prompt` (list[int])
  and samples straight from it via the engine's existing
  get_token_output_from_token_input + assemble_model_output, returning a
  completions-style body (prompt_token_ids + choices[].token_ids) — the shape
  the gateway's cumulative handler already extracts token IDs from. The chat
  (messages) path is unchanged.
- proxy: _handle_cumulative_non_streaming routes to local_handler when present
  (no HTTP worker); added _handle_cumulative_streaming_local to synthesize an
  SSE stream from the single in-process completion (mirrors
  _handle_streaming_local), with the same token ingest.

Net effect: Tinker rollouts get the same prefix-extension guarantee verl has —
turn N's prompt tokens are byte-for-byte prior turns' prompt+completion tokens,
so the sequence the optimizer trains on matches what was generated.

Tests (no Tinker service / model required):
- tests/test_tinker_adapter_cumulative.py — token-prompt path samples from
  tokens; chat path unchanged; non-int prompt falls through.
- rllm-model-gateway/tests/unit/test_cumulative_token_mode_local.py — proxy
  local cumulative non-streaming + streaming ingest + chat translation.
Existing cumulative/accumulator suites (28) still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(swe-rl): enable cumulative token mode + raise context window

Enables the in-process cumulative-token path (cherry-picked from #658) in the
tinker recipe for testing: rllm.gateway.cumulative_token_mode=true +
renderer_family=qwen3.5. Also raises training.max_length=65536 /
data.max_prompt_length=57344 so long mini-swe-agent trajectories fit, and
save_freq=10.

NOTE: the cumulative feature commit on this branch is a cherry-pick of #658 for
local testing — drop it (rebase onto main) once #658 merges to avoid duplication.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(swe-rl): add synchronous tinker training script

train_tinker_sync.sh: on-policy variant of train_tinker.sh for testing —
drops async_training (synchronous generate→train) and uses a real
data.train_batch_size (default 4; effective batch = train_batch_size x
group_size). Same model/sandbox/context/cumulative settings otherwise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(swe-rl): disable cumulative token mode (incompatible with thinking model)

Qwen3.5 is a thinking model (<think>...</think> per assistant turn). The normal
re-render path strips prior-turn reasoning from history (mini-swe-agent stores
the think-stripped message.content), but cumulative mode carries forward the RAW
completion tokens (tinker_engine completion_ids=response_tokens, incl. <think>),
which renderers.bridge_to_next_turn concatenates verbatim. The model then sees an
ever-growing stack of its own prior reasoning — out-of-distribution for multi-turn
— degrading into short, submit-early, zero-reward trajectories.

Renderer mismatch was ruled out: the bridge (PrimeIntellect qwen3.5) and the
engine renderer are token-identical; the bridged prompt is well-formed. The
issue is purely reasoning carry-forward. Cumulative mode is fine for
non-thinking models / single-turn; for thinking models, correct multi-turn
rollout requires stripping prior reasoning (i.e. re-tokenization), which is
fundamentally at odds with exact-token reuse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(swe-rl): switch to Qwen3.5-4B + terminus2 harness

- agent_flow -> Terminus2Harness (was MiniSweAgentHarness) in train.py;
  docstring/script prose updated accordingly.
- model.name / MODEL_PATH -> Qwen/Qwen3.5-4B; renderer_family -> qwen3.5;
  experiment names -> r2egym-terminus2-qwen3.5-4b[-sync/-verl].
- re-enable rllm.gateway.cumulative_token_mode in the tinker scripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(swe-rl): bump n_parallel_tasks to 128 (tinker + sync)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add train_fireworks.sh

Co-Authored-By: 1stprinciple <ztyharbin@gmail.com>

* Support FireworksEngine in gateway

Co-Authored-By: 1stprinciple <ztyharbin@gmail.com>

* add max_prompt_length limit

Co-Authored-By: 1stprinciple <ztyharbin@gmail.com>

* use tokenizer_model when exists.

Co-Authored-By: 1stprinciple <ztyharbin@gmail.com>

* style(gateway): fix ruff formatting (trailing whitespace)

Co-Authored-By: 1stprinciple <ztyharbin@gmail.com>

* feat(swe-rl): scale Fireworks rollout deployment to 4 replicas

Set fireworks_config.rollout_deployment_replica_count=4 in both the async
and sync Fireworks scripts (the prior fireworks_config.replica_count key
does not exist in the backend config).

Co-Authored-By: 1stprinciple <ztyharbin@gmail.com>

---------

Co-authored-by: 1stprinciple <ztyharbin@gmail.com>
…ks (#668)

* feat(algorithm): add ECHO env-observation loss across verl/tinker/fireworks

ECHO (arXiv:2605.24517) augments GRPO with an auxiliary cross-entropy loss on
environment-observation tokens — the tool/terminal output the policy conditions
on but that GRPO masks out of its loss. Advantages are identical to GRPO; the
only change is the extra loss term, so every rollout (including failures) becomes
dense supervision at no extra rollout cost.

Selectable via the unified trainer on all three backends:

    rllm.algorithm.adv_estimator=echo          # lambda defaults to paper's 0.05
    rllm.algorithm.env_loss_coef=<lambda>      # override; 0.0 == plain GRPO

Shared core (backend-agnostic):
- rLLMAdvantageEstimator.ECHO + env_loss_coef on AlgorithmConfig; None resolves
  to 0.05 for echo, else 0.0 (sentinel distinguishes unset from explicit 0).
- echo registered as a GRPO-advantage alias in the estimator registry.

Per-backend env-CE term (the only backend-specific part):
- verl: added in CustomPPOLoss, reusing the existing forward pass log-probs and
  GRPO's loss_agg_mode/global-batch normalization. Single pass, exact, free.
- tinker: gradient-accumulated cross_entropy pass (weights=lambda on obs tokens),
  returned separately so policy-pass logprobs stay aligned. Clean because
  tinker's optim_step applies no extra normalization.
- fireworks: same second pass, but normalization is folded into weights/advantages
  and GradAccNormalization is forced to NONE (its counting normalization spans
  passes and would otherwise rescale the policy gradient).

Defaults to 0.0 everywhere, so existing GRPO runs are unchanged. Adds ECHO config
tests and documents the switch in the swe-rl cookbook README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(design): auxiliary-loss framework (AuxiliaryLoss) + ECHO/SDAR clients

Design doc for a general token-level auxiliary-loss abstraction so algorithms
like ECHO and SDAR are a registered spec + config instead of bespoke per-backend
surgery. Covers: named token masks (data layer), the AuxiliaryLoss interface
(mask/target/weight/requires), a registry, the aux_losses config list, the two
backend executors (in-process for verl; gradient-accumulated cross_entropy pass
for tinker/fireworks), ECHO and gap-gated SDAR as reference clients, the
verl/managed capability model with graceful degradation, and an incremental
migration plan that ports the just-merged ECHO onto the framework first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(algorithm): generalize ECHO into an AuxiliaryLoss framework

Prototype of the auxiliary-loss abstraction from design/auxiliary-losses.md
(migration steps 1-3): token-level auxiliary losses become a registered spec +
config instead of bespoke per-backend code, and ECHO is ported onto it as the
first client. Behavior-preserving for ECHO.

Shared core (rllm/trainer/algorithms/aux_loss.py):
- AuxiliaryLoss spec (mask / weight / requires), register_aux_loss registry,
  EnvPredictionLoss, and build_aux_losses() with env_loss_coef / adv_estimator=echo
  back-compat sugar. New algorithm.aux_losses config list (a stack of
  {type, coef, ...} specs).

Backend executors (the only backend-specific part):
- verl: CustomPPOLoss._apply_aux_losses folds each term into the single existing
  forward/backward pass (replaces _add_env_loss).
- tinker: _get_aux_loss_futures submits one gradient-accumulated cross_entropy
  pass per loss (replaces _build_env_datum/_get_env_loss_futures).
- fireworks: _build_aux_datums + the normalization fold / GradAccNormalization.NONE
  now driven by build_aux_losses() rather than env_loss_coef.
- Shared managed datum builder in rllm/trainer/tinker/aux_loss.py
  (aux_positions + build_aux_ce_datum), used by both tinker and fireworks.

Adding a new aux loss is now one registered class + config, no per-backend
surgery. SDAR's dynamic-weight + teacher-forward extensions (the `requires`
field) are the next milestone and currently raise NotImplementedError.

Tests: tests/unified_trainer/test_aux_loss.py covers the registry, config
resolution + ECHO sugar, the managed datum builder, and the executor math (incl.
empty-observation safety). ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(tinker): satisfy ruff UP038 in aux_loss isinstance

Use `int | float` instead of `(int, float)` in the isinstance call
(rllm/trainer/tinker/aux_loss.py:51) to fix the pre-commit ruff failure
on PR #668.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(algorithms): rewrite RL algorithms overview + register in nav (#675)

The core-concepts/rl-algorithms page was stale (documented an old
kl_ctrl/AgentPPOTrainer API, only GRPO/PPO/ReMax) and unlinked from the nav.
Rewrite it as an accurate overview of the current algorithm surface:
- built-in advantage estimators (grpo, reinforce, reinforce_plus_plus_baseline,
  prpo, rloo, echo) with exact adv_estimator strings
- ECHO + the auxiliary-loss framework (env_loss_coef / aux_losses, per-backend
  cost, metrics)
- how to build custom RL algorithms: custom advantage estimator
  (@register_rllm_adv_estimator) and custom auxiliary loss (@register_aux_loss)
- config quick reference + cross-links to advantage-estimator / configuration /
  capability-matrix / backends

Registers the page under the "Advanced algorithms" nav group.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#674)

End-to-end terminal-agent RL recipe, sibling of cookbooks/swe-rl:
- train on a local set of Harbor-format terminal-agent tasks (a .tar.zst you
  provide via TB_TRAIN_TARBALL), eval on harbor:terminal-bench@<TB_EVAL_VERSION>
  (default 2.0; flip to 2.1 once published)
- terminus2 harness runs inside per-task sandboxes; per-task tests/test.sh
  is the verifier (writes /logs/verifier/reward.txt)
- tinker / tinker-sync / verl / fireworks / fireworks-sync train scripts
- README documents the ECHO switch (rllm.algorithm.adv_estimator=echo)

The training dataset is not public yet, so the README/scripts describe it
generically and take the tarball path from TB_TRAIN_TARBALL.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#677)

* feat(harness): configurable rollout limits — uncapped turns, longer timeout, Modal lifetime headroom

Stop cutting sandboxed CLI rollouts short, and make the limits configurable
rather than hardcoded:

- terminus2: max_turns defaults to None (no artificial cap — Harbor's own
  default) instead of 50. RLLM_TERMINUS_MAX_TURNS is emitted only when set,
  and the in-sandbox driver treats an absent value as uncapped.
- BaseCliHarness: per-rollout run_timeout default 1800 -> 3600s; configure()
  now consumes an `agent_timeout` override.
- eval CLI: add `--agent-timeout SECONDS` (routed through configure(); also
  published as RLLM_HARNESS_RUN_TIMEOUT_S so the sandbox lifetime tracks it).
- modal_backend: derive the sandbox lifetime as run_timeout + install_timeout
  + headroom so the container can't be reaped before the agent's own timeout
  fires — the exit-137 / "Sandbox already shut down" failure. The lifetime
  clock starts at create, before install + run + verify, so a flat default
  collided with the run timeout. RLLM_MODAL_SANDBOX_TIMEOUT_S still overrides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cookbooks): wire TERMINUS_MAX_TURNS knob + tune terminal-rl fireworks config

- terminal-rl & swe-rl train.py: read the TERMINUS_MAX_TURNS env knob
  (empty/0 = uncapped) and pass it to Terminus2Harness(max_turns=...).
- all train_*.sh: default TERMINUS_MAX_TURNS=100 (overridable per run), so the
  operational cap lives in the scripts rather than the harness.
- terminal-rl/train_fireworks.sh: grow the context window to match longer,
  uncapped rollouts — training.max_length 64k->128k, data.max_prompt_length
  56k->120k, enable gateway.cumulative_token_mode, and lower async
  staleness_threshold to 0.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Surface the loaded flow's effective knobs as a "Config" row in the
`rllm eval` header (alongside Benchmark / Model / Agent / Evaluator):
max turns, temperature, run/install timeouts, max concurrent.

Knobs are read off the agent instance via a curated (attr, label,
formatter) spec, so a flow that doesn't expose one simply omits that
chip and an agent exposing none gets no row — graceful across harnesses
(react, CLI agents) and harbor runtimes. Rendered as dim-key / bold-value
chips joined by a dim "·"; spaces within a chip are non-breaking so a
pair like "run timeout 3600s" never splits across a line wrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(fireworks): per-trajectory session affinity + prompt-cache metric

Fireworks reuses a replica's prompt-prefix KV only when a trajectory's turns
route to the same replica (keyed on a session id via request headers). The
rollout path set no such header, so multi-turn requests scattered across
replicas and re-prefilled the (growing) prompt every turn.

(a) Session affinity:
- Gateway threads the per-rollout session id into the in-process handler body
  (rllm_session_id) at all local-handler call sites.
- tinker_adapter forwards it only to engines advertising
  supports_session_affinity (Fireworks), so the tinker path is unchanged.
- FireworksEngine sets x-multi-turn-session-id / x-session-affinity per request.
  Since DeploymentSampler.async_completions_stream has no per-call header hook,
  _inference_headers is monkeypatched to merge headers stashed in an async-safe
  ContextVar set around each call.

(b) Prompt-cache metric:
- _record_prompt_cache accumulates ServerMetrics.cached_prompt_tokens /
  prompt_tokens, adds a per-call prompt_cache_hit_ratio to the metrics dict, and
  logs a rolling hit-ratio every 50 completions. Low ratio => affinity not
  pinning a trajectory to one replica.

Both are automatic — no config change. Verify by watching the
"Fireworks prompt cache: hit_ratio=..." log climb across a rollout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(fireworks): rename session headers + drop in-process prompt-cache recording

- Rename affinity_headers -> session_headers (the per-request
  x-multi-turn-session-id / x-session-affinity headers).
- Drop the in-process prompt-cache recording (_record_prompt_cache, the rolling
  counters, and the per-call prompt_cache_hit_ratio metric); read cache
  behaviour from the Fireworks console instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…n the picker

Two CLI UX fixes for the Fireworks eval path.

Sampling params:
- `rllm eval` now seeds temperature=1.0, top_p=1.0 (mirrors the cookbook
  training rollout), overridable by --sampling-params / --temperature / --top-p
  and always shown in the header's gateway-enforced "Sampling" row.
- Removed the duplicate temperature from the "Config" panel — it showed the
  harness's requested value (0.7) while the gateway enforced a different one
  (e.g. 1.0), so the same param appeared twice with conflicting values.
- Terminus2Harness default temperature 0.7 -> 1.0 (and the driver fallback) so
  nothing lingers at 0.7 when sampling isn't gateway-enforced.

Fireworks deployment fetch:
- fetch_fireworks_deployed_models only queried the deployedModels endpoint
  (populated when a custom/fine-tuned model is bound to a deployment), so a
  dedicated deployment of a base model — which has no deployedModel entry and
  is addressed for inference by its accounts/<acct>/deployments/<id> name — was
  invisible in the `rllm setup` picker, even though that's the exact id evals
  run against. Now also lists READY deployments by their id (e.g. gpdl1exc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agentflow_engine tqdm bar only updated the completed count; add a
set_postfix with the cumulative mean episode reward so the current score
is visible live during a run instead of only per-rollout lines. Falls
back to is_correct for error/empty episodes. Display only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… metrics, prompt-cache affinity

Terminal-bench eval on Modal scored ~30-33% vs official Terminus-2 ~40%, with
flaky "Command failed" warnings. Root cause was infra, not model quality.

The Modal sandbox lifetime was derived from a global env var (a flat 4800s),
while the agent runs for the per-task agent_timeout (600-12000s) and the
verifier then runs in the same box. 14 of 89 terminal-bench@2.0 tasks have
agent+verifier > 4800s, so the box was reaped before the verifier's exec
("Sandbox already shut down") -> the whole episode became a 0-step ERROR
(all agent work discarded). With retry_limit=1, each became a permanent zero.

Changes:
- eval/_resolution: size the Modal sandbox per-task, max(override,
  agent_timeout + verifier_timeout + install + slack), so the box always
  outlives the agent + verifier it hosts.
- eval/runner: eval retry_limit 1 -> 2, so transient infra failures get one
  retry instead of becoming permanent zeros (matches the official harness).
- engine/agentflow_engine: live running accuracy/reward on the progress bar
  (acc N/done=% reward=...) so the score is visible as the eval runs.
- sandbox/protocol + modal_backend: classify an exec killed at its own timeout
  as SandboxCommandTimeout instead of a generic "Command failed (exit -1)";
  cli_harness catches it and logs at INFO ("reached its time budget") — an
  agent spending its full per-task budget is expected, the verifier still
  scores the partial state.
- gateway proxy: inject x-session-affinity / x-multi-turn-session-id from the
  rollout session id on the HTTP path, so eval multi-turn trajectories reuse
  Fireworks' per-replica prompt cache. Previously only the in-process training
  path set affinity; eval (HTTP proxy path) sent none.

Note: modal_backend.py also carries a pre-existing _CreateRateLimiter (Modal
create-rate pacing) that shares the new `import time`; it is bundled here
because it lives in the same file and is also eval-reliability related.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ask agent_timeout

BaseCliHarness.run() resolved the rollout exec timeout as
`task.metadata.get("agent_timeout", self.run_timeout)`, so a task carrying a
per-task agent_timeout always won and an operator-set
RLLM_HARNESS_RUN_TIMEOUT_S was only a fallback that never fired. Harbor tasks
always carry one — task_from_row merges task.toml's [agent].timeout_sec into
metadata — so e.g. training tb-opus-pass with RLLM_HARNESS_RUN_TIMEOUT_S=1800
silently ran rollouts to the task's 3600s budget. The intended wall-clock cap
was ignored.

Make run_timeout a hard ceiling when an operator sets it (env var or
--agent-timeout): effective timeout = min(per-task agent_timeout, run_timeout).
When it's unset, the task's own agent_timeout governs, so eval still honors each
benchmark's per-task budget (no regression to the terminal-bench parity work).
A run_timeout_is_cap flag (captured from the env at import, flipped on by
configure() for --agent-timeout) distinguishes the two.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xy resilience (#684)

* feat(sandbox/modal): isolate each run's sandboxes by run id

Name the Modal App per-run (rllm-sandbox-<run_id>) and stamp each sandbox with a name + rllm_run_id tag, so on a shared account you can list/terminate just one run's (leaked) sandboxes via 'modal app stop rllm-sandbox-<run_id>'. run_id comes from RLLM_RUN_ID (sanitized) or a random per-process id (new rllm.env.rllm_run_id()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(eval): proxy-death monitor, empty-completion canary, streaming UI uploads

proxy.py: capture the litellm proxy's stdout+stderr (was DEVNULL) and add a monitor thread that loudly reports if the proxy dies mid-run — previously a dead proxy was silent and every rollout returned empty with no visible cause.

agentflow_engine.py: flag rollouts whose LLM completions are ALL empty (upstream down — dead proxy / no healthy workers / model) and stream each enriched+scored episode to the UI as it finishes.

runner.py + tracking.py: invoke on_episode_complete streaming (per-rollout) instead of an end-of-run burst; make the UI drain timeout env-tunable (RLLM_UI_DRAIN_TIMEOUT_S, default 600s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(deps): bump fireworks-ai to 1.2.0a83

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(gateway): raise worker health-check timeout 5s -> 20s

Under a concurrency spike the single litellm proxy worker is slow to answer /health while busy forwarding requests; a 5s timeout marked it dead (3 strikes) and route() then raised 'No healthy workers available', failing agent turns. 20s tolerates the transient slowness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eval): auto-pick free port for eval proxy so concurrent jobs don't collide

The auto-started LiteLLM proxy hardcoded port 4000, so two concurrent `rllm eval` jobs collided on it — and the loser's startup probe could see the winner's proxy answering and then clobber its routing via /admin/reload.

EvalProxyManager now binds a free port by default (proxy_port=None), with a retry-on-bind-race so concurrent launches each settle on their own port. Add --proxy-port to pin a deterministic port when debugging a single run. The gateway layer was already free-port, so port 4000 was the only collision point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: signalrush <269811712+signalrush@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… pass)

ECHO's environment-prediction term was applied as a second, gradient-
accumulated `cross_entropy` forward_backward on top of the builtin GRPO
kernel pass. The managed builtin kernel can only express the policy loss,
so the aux term needed its own pass.

When an auxiliary loss is active, switch to the client loss path: a single
`forward_backward_custom` whose closure computes the cookbook policy loss
(`build_loss_fn`) plus each aux term from the one forward's per-token
logprobs. The accumulated gradient is identical to the old two-pass path
(advantages carry the policy normalization optim would apply; each obs
token carries the same `coef/(n_seq*|O_i|)` weight the cross_entropy pass
used), for one fewer backward per step. Non-ECHO runs keep the fast
builtin kernel path. Tinker is unchanged.

- new `_forward_backward_client_with_aux` + module-level `_fold_aux_into_loss`
- remove now-dead `_build_aux_datums`
- tests: fold reproduces paper L_env; fold adds the term with correct
  gradient (d/dlogp = -weights) and skips obs-free datums

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

3 participants