From 9ee2cf9e710355e972b47c1159d8d9d061e87bd0 Mon Sep 17 00:00:00 2001 From: michael <78113554+chowhsu@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:52:20 +0000 Subject: [PATCH 1/2] rlf-codex-rl 5.8 attempt 2: training attention backend override + tape-surviving gate evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --rl-training-attention-backend {auto,flash,fused,unfused}: recipe default overridable (flash-only recipes refuse); gate run uses unfused after the L40S flash-attn cute JIT crd2idx compile failure (failure class trainer_kernel_compile, attention fwd, LoRA-independent) - rlf_rl_gate: policy-update proof from the island event tape rl_publication payload hashes (代拍板 v2) — survives island teardown via --modal-tape-volume; adapter-file path kept; policy_update_source recorded in evidence - CPU tests: 4 new backend-override + 5 new tape-collection tests Co-authored-by: Cursor --- tests/test_rl_training_attention_backend.py | 144 ++++++++++++++++++++ tests/test_rlf_rl_gate.py | 56 ++++++++ yeto/cli.py | 8 ++ yeto/launcher.py | 7 +- yeto/rl/adapters/miles/island_entry.py | 5 + yeto/rl/engine/run_config.py | 13 ++ yeto/rl/eval/rlf_rl_gate.py | 82 ++++++++++- 7 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 tests/test_rl_training_attention_backend.py diff --git a/tests/test_rl_training_attention_backend.py b/tests/test_rl_training_attention_backend.py new file mode 100644 index 000000000..6ce0027a0 --- /dev/null +++ b/tests/test_rl_training_attention_backend.py @@ -0,0 +1,144 @@ +"""--rl-training-attention-backend override (rlf-codex-rl 5.8). + +The recipe picks the Megatron --attention-backend (qwen3_5 -> flash, +generic -> unfused). The 5.8 gate run hit a flash-attn cute JIT compile +failure on L40S, so the CLI gained an explicit override; flash-only +recipes refuse a non-flash override. +""" + +from __future__ import annotations + +import argparse +import sys +import types + +import pytest + +from yeto.rl.adapters.miles.island_entry import build_miles_argv + + +def _args(**extra): + base = dict( + seq_len=128, + groups_per_round=4, + samples_per_group=2, + optimizer_steps=1, + lora_targets="auto", + lora_r=4, + seed=1, + learner_id=0, + global_rounds=3, + inner_lr=1e-4, + reward_function="pkg.reward:score", + over_sampling_batch_size=6, + rollout_max_response_len=64, + custom_generate_function_path="pkg.agent.generate", + custom_agent_function_path="pkg.agent.run", + agent_max_seq_len=96, + use_session_server=True, + session_server_ip="127.0.0.1", + session_server_port=[31000, 31002], + tito_model="qwen3", + tito_allowed_append_roles=["tool", "user"], + actor_num_nodes=1, + actor_num_gpus_per_node=8, + expert_parallel=None, + apply_chat_template_kwargs={"enable_thinking": False}, + ) + base.update(extra) + return argparse.Namespace(**base) + + +def _provider(**extra): + base = dict( + hidden_size=16, + num_attention_heads=4, + num_layers=2, + ffn_hidden_size=32, + num_query_groups=2, + kv_channels=4, + seq_length=256, + vocab_size=64, + normalization="RMSNorm", + layernorm_epsilon=1e-6, + position_embedding_type="yarn", + rotary_base=10000, + gated_linear_unit=True, + share_embeddings_and_output_weights=False, + add_bias_linear=False, + add_qkv_bias=True, + qk_layernorm=True, + num_moe_experts=8, + moe_ffn_hidden_size=24, + moe_router_topk=2, + moe_layer_freq=[0, 1], + moe_shared_expert_intermediate_size=12, + multi_latent_attention=True, + q_lora_rank=8, + kv_lora_rank=4, + qk_head_dim=4, + qk_pos_emb_head_dim=2, + v_head_dim=4, + ) + base.update(extra) + return argparse.Namespace(**base) + + +@pytest.fixture(autouse=True) +def _chat_template_stub(monkeypatch): + monkeypatch.setitem( + sys.modules, + "miles.utils.chat_template_utils", + types.SimpleNamespace( + resolve_reasoning_and_tool_call_parser=lambda model: ("qwen3", "qwen25") + ), + ) + + +def _backend_of(argv): + return argv[argv.index("--attention-backend") + 1] + + +def test_gdn_recipe_default_stays_flash(): + argv = build_miles_argv( + _args(), + model_path="/model", + prompt_path="/prompts.jsonl", + provider=_provider(experimental_attention_variant="gated_delta_net"), + target_modules=["qkv_proj", "out_proj"], + ) + assert _backend_of(argv) == "flash" + + +def test_override_unfused_on_gdn_recipe(): + argv = build_miles_argv( + _args(rl_training_attention_backend="unfused"), + model_path="/model", + prompt_path="/prompts.jsonl", + provider=_provider(experimental_attention_variant="gated_delta_net"), + target_modules=["qkv_proj", "out_proj"], + ) + assert _backend_of(argv) == "unfused" + + +def test_override_fused_on_generic_recipe(): + argv = build_miles_argv( + _args(rl_training_attention_backend="fused"), + model_path="/model", + prompt_path="/prompts.jsonl", + provider=_provider(), + target_modules=["qkv_proj", "out_proj"], + ) + assert _backend_of(argv) == "fused" + + +def test_auto_default_when_attr_missing(): + # Old launch-side namespaces (and most tests) never set the attribute. + argv = build_miles_argv( + _args(), + model_path="/model", + prompt_path="/prompts.jsonl", + provider=_provider(), + target_modules=["qkv_proj", "out_proj"], + ) + assert _backend_of(argv) == "unfused" # generic recipe default diff --git a/tests/test_rlf_rl_gate.py b/tests/test_rlf_rl_gate.py index 38f0de386..a63c4929f 100644 --- a/tests/test_rlf_rl_gate.py +++ b/tests/test_rlf_rl_gate.py @@ -19,6 +19,7 @@ GateEvidenceError, build_gate_rows, collect_policy_update, + collect_policy_update_from_tape, collect_rollout_samples, prepare_prompt_data, run_gate, @@ -164,6 +165,61 @@ def test_real_collector_passing_run_announces_verdict(tmp_path): evidence["policy_update_evidence"]["after_digest"] +# -- policy-update proof from the island event tape (代拍板 v2) ----------------- + +def _tape(tmp_path, events): + path = tmp_path / "rl-island-0.jsonl" + path.write_text("".join(json.dumps(e) + "\n" for e in events), encoding="utf-8") + return path + + +def _publication(version, digest): + return {"event": "rl_publication", "policy_version": version, + "sync/publication_payload_hash": digest} + + +def test_tape_policy_update_detects_changed_payload(tmp_path): + tape = _tape(tmp_path, [ + {"event": "rl_driver_start"}, + _publication(0, "aaa"), + "not json at all", + _publication(1, "bbb"), + _publication(2, "ccc"), + ]) + update = collect_policy_update_from_tape(tape, step=2) + assert update.updated is True + assert update.before_digest == "aaa" and update.after_digest == "ccc" + assert update.changed == ("policy_payload",) + + +def test_tape_policy_update_unchanged_payload_marks_not_updated(tmp_path): + tape = _tape(tmp_path, [_publication(0, "aaa"), _publication(1, "aaa")]) + update = collect_policy_update_from_tape(tape, step=1) + assert update.updated is False and update.changed == () + + +def test_tape_policy_update_fails_closed_on_single_version(tmp_path): + tape = _tape(tmp_path, [_publication(0, "aaa")]) + with pytest.raises(GateEvidenceError, match="published policy version"): + collect_policy_update_from_tape(tape, step=1) + + +def test_tape_policy_update_fails_closed_on_missing_hash(tmp_path): + tape = _tape(tmp_path, [_publication(0, "aaa"), {"event": "rl_publication", + "policy_version": 1}]) + with pytest.raises(GateEvidenceError, match="payload hash"): + collect_policy_update_from_tape(tape, step=1) + + +def test_tape_policy_update_passes_full_gate(tmp_path): + samples = collect_rollout_samples("ckpt", loader=lambda _p: _payload(_passing_groups())) + tape = _tape(tmp_path, [_publication(0, "aaa"), _publication(1, "bbb")]) + update = collect_policy_update_from_tape(tape, step=1) + rows = build_gate_rows(samples, update) + evidence = run_gate(rows, collector="miles_island", update=update) + assert evidence["verdict"] is True + + # -- evidence on disk: content and flag hygiene -------------------------------- def test_write_evidence_scrubs_flags_and_keeps_detail(tmp_path): diff --git a/yeto/cli.py b/yeto/cli.py index 42698b28f..64d39beb7 100644 --- a/yeto/cli.py +++ b/yeto/cli.py @@ -138,6 +138,14 @@ def _add_launch_args(p: argparse.ArgumentParser) -> None: default="generic", help="model-specific Miles memory/kernel contract", ) + rl.add_argument( + "--rl-training-attention-backend", + choices=["auto", "flash", "fused", "unfused"], + default="auto", + help="override the recipe's Megatron --attention-backend (auto = recipe " + "decides); use unfused when the flash-attn cute JIT cannot compile on " + "the target GPU. Recipes with flash-only kernels refuse non-flash.", + ) rl.add_argument( "--expert-full-count", type=int, diff --git a/yeto/launcher.py b/yeto/launcher.py index 00ca43691..a37c2d69a 100644 --- a/yeto/launcher.py +++ b/yeto/launcher.py @@ -3968,7 +3968,12 @@ def make_miles_island_task( flags = ( f" --model {shlex.quote(args.model)}" f" --rl-model-recipe {shlex.quote(args.rl_model_recipe)}" - f" --data {shlex.quote(learner_data_arg(args.data))}" + + ( + f" --rl-training-attention-backend {args.rl_training_attention_backend}" + if getattr(args, "rl_training_attention_backend", "auto") not in (None, "auto") + else "" + ) + + f" --data {shlex.quote(learner_data_arg(args.data))}" + ( " --rl-single-island-no-sync" if getattr(args, "rl_single_island_no_sync", False) diff --git a/yeto/rl/adapters/miles/island_entry.py b/yeto/rl/adapters/miles/island_entry.py index cd1a38b9b..2122b9463 100644 --- a/yeto/rl/adapters/miles/island_entry.py +++ b/yeto/rl/adapters/miles/island_entry.py @@ -35,6 +35,11 @@ def parse_args(argv=None): choices=["generic", "deepseek-v4-flash"], default="generic", ) + parser.add_argument( + "--rl-training-attention-backend", + choices=["auto", "flash", "fused", "unfused"], + default="auto", + ) parser.add_argument("--expert-full-count", type=int, default=0) parser.add_argument("--expert-full-lr", type=float, default=1e-6) parser.add_argument("--expert-selection-sha256", default=None) diff --git a/yeto/rl/engine/run_config.py b/yeto/rl/engine/run_config.py index baf08188a..48e03d0ab 100644 --- a/yeto/rl/engine/run_config.py +++ b/yeto/rl/engine/run_config.py @@ -738,6 +738,19 @@ def resolve_rl_run_config( else: recipe_name, attention_backend = RECIPE_GENERIC, "unfused" + # rlf-codex-rl 5.8: --rl-training-attention-backend overrides the recipe's + # default (e.g. flash -> unfused when the flash-attn cute JIT fails to + # compile on a given GPU). Recipes whose kernels hard-require flash + # attention refuse any other backend. + backend_override = getattr(args, "rl_training_attention_backend", "auto") or "auto" + if backend_override != "auto": + if recipe_name in {RECIPE_DEEPSEEK_V4_FLASH, RECIPE_QWEN3_8_NEXT} and backend_override != "flash": + raise ValueError( + f"the {recipe_name} recipe requires --rl-training-attention-backend flash " + "(its kernels have no unfused/fused path)" + ) + attention_backend = backend_override + _validate_callable_spec(args.reward_function) eval_config = _resolve_eval( diff --git a/yeto/rl/eval/rlf_rl_gate.py b/yeto/rl/eval/rlf_rl_gate.py index c33bd6041..f1089a5b4 100644 --- a/yeto/rl/eval/rlf_rl_gate.py +++ b/yeto/rl/eval/rlf_rl_gate.py @@ -44,6 +44,13 @@ and needs no fork patch; it cannot attribute the change to a specific optimizer step, which the gate does not require. +代拍板 v2 (2026-10-11): after gate attempt 1 lost every island-side file +on teardown, the collection of record is the island event tape's +``rl_publication`` payload hashes (initial vs final policy version) — see +:func:`collect_policy_update_from_tape`. The adapter-file path remains as +``--adapter-before/--adapter-after`` for runs that arrange adapter +snapshots themselves. + Flag hygiene: trusted flags live only in ``YETO_RLF_CTF_FLAGS_JSON`` (the Modal Secret). Every persisted evidence string is scrubbed through ``yeto.rl.rlf.modal.scrub`` with the flag values; flags are never logged. @@ -226,6 +233,60 @@ def collect_policy_update( return policy_update_evidence(dict(load(before_path)), dict(load(after_path)), step=step) +def collect_policy_update_from_tape( + tape_path: str | Path, + *, + step: int, +) -> PolicyUpdateEvidence: + """Policy-update proof from the island event tape's ``rl_publication`` records. + + 代拍板 v2 (2026-10-11, gate attempt 2): the first gate run lost every + island-side artifact when the island was torn down after the trainer + crash. The event tape is the one channel that is mirrored to a Modal + Volume (``--modal-tape-volume``) and pulled even on failure, and the + driver publishes one ``rl_publication`` event per policy version with a + learner-computed payload hash. Comparing the lowest- and + highest-version payload hashes proves an optimizer step changed policy + parameters without any island-side file surviving — the same + step-attribution limitation as the initial-vs-final adapter digest + (代拍板 v1), with strictly better survivability. + + Fail closed: fewer than two distinct published policy versions, or a + publication without a payload hash, aborts the gate. + """ + publications: dict[int, str] = {} + with Path(tape_path).expanduser().open(encoding="utf-8") as handle: + for line in handle: + line = line.strip() + if not line: + continue + try: + event = json.loads(line) + except json.JSONDecodeError: + continue # prefixed/garbled lines are not gate evidence + if not isinstance(event, Mapping) or event.get("event") != "rl_publication": + continue + version = event.get("policy_version") + digest = event.get("sync/publication_payload_hash") + if not isinstance(version, int) or not isinstance(digest, str) or not digest: + raise GateEvidenceError( + f"rl_publication event lacks policy_version/payload hash: {event!r}" + ) + publications.setdefault(version, digest) + if len(publications) < 2: + raise GateEvidenceError( + f"event tape has {len(publications)} published policy version(s); " + "the gate needs the initial and at least one post-training publication" + ) + first, last = min(publications), max(publications) + before_digest, after_digest = publications[first], publications[last] + updated = before_digest != after_digest + return PolicyUpdateEvidence( + int(step), updated, before_digest, after_digest, + ("policy_payload",) if updated else (), + ) + + # ------------------------------------------------------------------ gate rows def build_gate_rows( @@ -326,8 +387,12 @@ def main(argv: list[str] | None = None) -> None: help="path to the trusted flags JSON (never logged)") gate = sub.add_parser("gate", help="collect island artifacts, judge, write evidence") gate.add_argument("--completed-groups", required=True) - gate.add_argument("--adapter-before", required=True) - gate.add_argument("--adapter-after", required=True) + gate.add_argument("--adapter-before", default=None) + gate.add_argument("--adapter-after", default=None) + gate.add_argument("--tape-events", default=None, + help="island event tape (rl-island-*.jsonl): policy-update proof " + "from rl_publication payload hashes (代拍板 v2); mutually " + "exclusive with --adapter-before/--adapter-after") gate.add_argument("--step", type=int, required=True) gate.add_argument("--out", required=True) gate.add_argument("--flags-json", default=None) @@ -345,9 +410,20 @@ def main(argv: list[str] | None = None) -> None: prepare_prompt_data(config, args.out) return samples = collect_rollout_samples(args.completed_groups) - update = collect_policy_update(args.adapter_before, args.adapter_after, step=args.step) + adapter_pair = (args.adapter_before, args.adapter_after) + if args.tape_events and any(adapter_pair): + raise SystemExit("--tape-events is mutually exclusive with --adapter-before/--adapter-after") + if args.tape_events: + update = collect_policy_update_from_tape(args.tape_events, step=args.step) + update_source = "event_tape" + elif all(adapter_pair): + update = collect_policy_update(args.adapter_before, args.adapter_after, step=args.step) + update_source = "adapter_files" + else: + raise SystemExit("policy-update proof needs --tape-events or --adapter-before + --adapter-after") rows = build_gate_rows(samples, update) evidence = run_gate(rows, collector="miles_island", update=update) + evidence["policy_update_source"] = update_source write_evidence(evidence, args.out, secrets=secrets) if not evidence["verdict"]: raise SystemExit(f"RL gate failed: {evidence['gate']}") From c62c95c4ab0e9191b67241fc7d840c1383b70399 Mon Sep 17 00:00:00 2001 From: michael <78113554+chowhsu@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:09:35 +0000 Subject: [PATCH 2/2] rlf-codex-rl 5.8 attempt 3: --session-server-workers flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gate attempt 2 (rlf-gate-20261011-4) passed the attention-backend override (AttnBackend.unfused on the island) but died on a session-server import storm: Miles defaults --session-server-workers to 32, and 32 parallel sglang imports on the 4-core L40S island missed the 300 s ready window (same failure class as compaction G3 run c). Yeti had no flag for the worker count; plumb it cli -> launcher -> island -> AgentConfig -> Miles argv, launch-side >=1 validation, translation allowlist. The gate run drops to 8 workers (rollout is 4 groups x 4 samples). Silver lining: --modal-tape-volume + completed-groups in ~/yeto-output worked — the island tape was pulled despite the failed run. Co-authored-by: Cursor --- tests/test_rl_training_attention_backend.py | 22 +++++++++++++++++++++ yeto/cli.py | 8 ++++++++ yeto/launcher.py | 9 +++++++-- yeto/rl/adapters/miles/config.py | 3 +++ yeto/rl/adapters/miles/island_entry.py | 3 +++ yeto/rl/engine/run_config.py | 8 ++++++++ 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/test_rl_training_attention_backend.py b/tests/test_rl_training_attention_backend.py index 6ce0027a0..fc75f5298 100644 --- a/tests/test_rl_training_attention_backend.py +++ b/tests/test_rl_training_attention_backend.py @@ -142,3 +142,25 @@ def test_auto_default_when_attr_missing(): target_modules=["qkv_proj", "out_proj"], ) assert _backend_of(argv) == "unfused" # generic recipe default + + +def test_session_server_workers_forwarded_when_set(): + argv = build_miles_argv( + _args(session_server_workers=8), + model_path="/model", + prompt_path="/prompts.jsonl", + provider=_provider(), + target_modules=["qkv_proj", "out_proj"], + ) + assert argv[argv.index("--session-server-workers") + 1] == "8" + + +def test_session_server_workers_absent_by_default(): + argv = build_miles_argv( + _args(), + model_path="/model", + prompt_path="/prompts.jsonl", + provider=_provider(), + target_modules=["qkv_proj", "out_proj"], + ) + assert "--session-server-workers" not in argv diff --git a/yeto/cli.py b/yeto/cli.py index 64d39beb7..a88f392ad 100644 --- a/yeto/cli.py +++ b/yeto/cli.py @@ -706,6 +706,14 @@ def _add_launch_args(p: argparse.ArgumentParser) -> None: rl.add_argument("--use-session-server", action="store_true") rl.add_argument("--session-server-ip", default=None) rl.add_argument("--session-server-port", type=int, nargs="+", default=None) + rl.add_argument( + "--session-server-workers", + type=int, + default=None, + help="Miles session server instance count (default: Miles' 32); lower it " + "on few-core single-GPU islands to avoid the parallel-import storm " + "(32 sglang imports can miss the 300 s ready window)", + ) rl.add_argument("--tito-model", default=None) rl.add_argument( "--codex-backend-profile", diff --git a/yeto/launcher.py b/yeto/launcher.py index a37c2d69a..8496db699 100644 --- a/yeto/launcher.py +++ b/yeto/launcher.py @@ -2627,13 +2627,14 @@ def _prepare_rl_args( if not args.use_session_server and ( args.session_server_ip is not None or args.session_server_port is not None + or getattr(args, "session_server_workers", None) is not None or args.tito_model is not None or getattr(args, "codex_backend_profile", None) is not None or getattr(args, "tito_allowed_append_roles", None) is not None ): raise ValueError( - "--session-server-ip/--session-server-port/--tito-model/" - "--codex-backend-profile/" + "--session-server-ip/--session-server-port/--session-server-workers/" + "--tito-model/--codex-backend-profile/" "--tito-allowed-append-roles requires --use-session-server" ) roles = getattr(args, "tito_allowed_append_roles", None) @@ -2650,6 +2651,8 @@ def _prepare_rl_args( raise ValueError( "--session-server-port requires one positive port or an increasing range" ) + if getattr(args, "session_server_workers", None) is not None and int(args.session_server_workers) < 1: + raise ValueError("--session-server-workers must be at least 1") _default_session_server_port(args) specs = [rl_island_spec(args, spec) for spec in parse_gpu_spec(args.gpu)] @@ -4106,6 +4109,8 @@ def make_miles_island_task( if args.session_server_port: ports = " ".join(str(port) for port in args.session_server_port) flags += f" --session-server-port {ports}" + if getattr(args, "session_server_workers", None) is not None: + flags += f" --session-server-workers {int(args.session_server_workers)}" if args.tito_model: flags += f" --tito-model {shlex.quote(args.tito_model)}" if getattr(args, "codex_backend_profile", None): diff --git a/yeto/rl/adapters/miles/config.py b/yeto/rl/adapters/miles/config.py index 89ab3f8d2..a9ea68f1c 100644 --- a/yeto/rl/adapters/miles/config.py +++ b/yeto/rl/adapters/miles/config.py @@ -449,6 +449,7 @@ def _check_sglang_tp(value, config): "agent.use_session_server": _ok, "agent.session_server_ip": _ok, "agent.session_server_port": _ok, + "agent.session_server_workers": _ok, "agent.tito_model": _ok, "agent.tito_allowed_append_roles": _stock_codex_append_roles_or_none( "upstream Miles has no --tito-allowed-append-roles; the append roles are " @@ -972,6 +973,8 @@ def translate_run_config( if agent.session_server_port: values.append("--session-server-port") values.extend(str(port) for port in agent.session_server_port) + if agent.session_server_workers is not None: + values.extend(("--session-server-workers", str(agent.session_server_workers))) if agent.tito_model: resolver = tito_parser_resolver or _resolve_tito_parsers values.extend(("--tito-model", agent.tito_model)) diff --git a/yeto/rl/adapters/miles/island_entry.py b/yeto/rl/adapters/miles/island_entry.py index 2122b9463..32b63fad1 100644 --- a/yeto/rl/adapters/miles/island_entry.py +++ b/yeto/rl/adapters/miles/island_entry.py @@ -131,6 +131,7 @@ def parse_args(argv=None): parser.add_argument("--use-session-server", action="store_true") parser.add_argument("--session-server-ip", default=None) parser.add_argument("--session-server-port", type=int, nargs="+", default=None) + parser.add_argument("--session-server-workers", type=int, default=None) parser.add_argument("--tito-model", default=None) parser.add_argument("--codex-backend-profile", default=None) parser.add_argument("--codex-exclude-reasoning-from-loss", action="store_true") @@ -1740,6 +1741,8 @@ def _legacy_miles_argv(config) -> list[str]: if agent.session_server_port: values.append("--session-server-port") values.extend(str(port) for port in agent.session_server_port) + if agent.session_server_workers is not None: + values.extend(("--session-server-workers", str(agent.session_server_workers))) if agent.tito_model: from miles.utils.chat_template_utils import ( resolve_reasoning_and_tool_call_parser, diff --git a/yeto/rl/engine/run_config.py b/yeto/rl/engine/run_config.py index 48e03d0ab..365280abc 100644 --- a/yeto/rl/engine/run_config.py +++ b/yeto/rl/engine/run_config.py @@ -417,6 +417,9 @@ class AgentConfig: session_server_port: tuple[Any, ...] | None tito_model: str | None tito_allowed_append_roles: tuple[str, ...] | None + # last + defaulted: direct AgentConfig constructors in older tests/helpers + # (keyword-style, without this field) keep working + session_server_workers: int | None = None @dataclass(frozen=True) @@ -973,6 +976,11 @@ def resolve_rl_run_config( use_session_server=use_session_server, session_server_ip=session_ip, session_server_port=tuple(session_port) if session_port else None, + session_server_workers=( + int(args.session_server_workers) + if getattr(args, "session_server_workers", None) is not None + else None + ), tito_model=tito_model, tito_allowed_append_roles=tuple(append_roles) if append_roles else None, ),