Offline Eval Garbled Output / All-Zero Reward
A standalone val_only eval emits garbled output even for a known-good checkpoint — the colocated verl serving path, not the model
You have a trained MoE checkpoint (e.g. Qwen3-30B-A3B). Training rollouts are
coherent and rewards move. But when you run a standalone val_only eval of
the merged checkpoint, every trajectory is garbled repeated-token output ("Homo Homo Homo … entityType … @foreach") and every reward is 0.0 — including for the base
model, which cannot possibly be corrupt on disk.
This page is the distilled runbook. Full dev write-up: session
5c94cb60 (2026-07-02).
Symptom
proxy_trajectory.jsonassistant messages are uniform repeated-token noise, identical across astropy/django/sympy tasks.- Responses hit the
max_response_lengthceiling and never emit EOS; a directcurlof the eval's vLLM (/v1/chat/completions, temp 0, "reverse a string") returns e.g.崛起崛起崛起…orHomo Homo…. - vLLM shows requests piling up past the concurrency cap with KV cache climbing to ~90% — the no-EOS signature.
- Base-model (step 0) eval is also garbage → rules out the checkpoint/merge.
Root cause — the colocated verl serving path
The standalone eval (verl.trainer.main_ppo --val_only) is colocated
(hybrid_engine=True): vLLM and the actor share the same GPUs. vLLM launches
with --load_format dummy --enable_sleep_mode, i.e. it loads random weights,
then sleeps and discards them to free the GPU, and relies on the actor to
sync the real weights back on wake-up. That dummy→real sync mis-maps Qwen3's
fused grouped-MoE experts, so vLLM generates with near-random FFNs → garbled output.
It is independent of what checkpoint you feed it.
Things that are not the cause (all ruled out empirically):
- Not the merge / checkpoint. Plain vLLM reading the same on-disk weights
serves coherent code. The
_unfuse_moe_expertsfix inmerge_veomni_fsdp_to_hf.pyis real and required (vLLM wants HF per-expert layout), but it is not what corrupts the eval output. - Not
strategy=fsdpvsmodel_engine=veomni. Switching the eval to the veomni engine still produces the sameHomooutput for the base model. - Not
enforce_eager. Both cudagraph andenforce_eager=Trueproduce garbled output. - Setting
enable_sleep_mode=False(to match training) OOMs — colocated vLLM stays resident at ~122 GiB and the actor has no room.
Why training rollouts are fine: training is disaggregated
(hybrid_engine=False, separate rollout.nnodes / trainer.nnodes). vLLM
owns dedicated nodes, weights stay resident, nothing sleeps or re-syncs. So the
colocated eval is a dead end: sleep=True → garbled output, sleep=False → OOM.
Fix — bypass verl entirely: harbor-native plain vLLM
Serve the merged checkpoint with a plain vllm serve --load-format auto
(real weights straight off disk, verified coherent) and drive the OpenHands-SDK
agent with harbor run. No actor, no dummy weights, no sleep/wake sync.
This path is now packaged as the eval runner — see Evaluation; the invocation below is the original script it grew out of.
GEN_TP=1 \
MODEL_PATH=/mnt/ydu/data/eval_models/openswe1031_veomni_0629/global_step_20_hf \
DATASET_PATH=/mnt/public/coding_rl/harbor_swe_val_tasks_official \
K8S_KUBECONFIG=/mnt/ydu/k8s-cpu-243-244.yaml \
N_CONCURRENT=16 POD_PREFIX=evalhn STEP_TAG=20 \
nohup bash scripts/eval_openswe_harbor_native.sh > eval_hn_step20.out 2>&1 &This produces real scores (Qwen3-30B-A3B step-20 ≈ 31% pass); trajectories are
proper agentic tool-calls (terminal: find … loader.py) with reward=1 on
solved tasks.
Operational pitfalls (all handled in eval_openswe_harbor_native.sh)
- Cluster.
k8s-cpu-240was a single node withDiskPressure=True+NoScheduletaint → pods Pending forever. Usek8s-cpu-243-244(6 healthy nodes). Nokubectlon the GPU host; query with thekubernetespython client, and uselist_namespaced_pod("default")—list_pod_for_all_namespacestimes out. - CPFS-slow venv →
GEN_TP=1. The.venvlives on CPFS; each vLLM worker imports it in ~5 min, so a TP≥2 multiproc launch times out its TCPStore rendezvous and crashes (WorkerProc initialization failed). UseGEN_TP=1— a 30B-A3B (~61 GiB) fits on one 140 GiB GPU with no multiprocessing. Copying the venv local is impractical (tens of thousands of small files on CPFS); a different local venv is version hell (transformers/starlette skew). - Harbor config is YAML/JSON, not TOML.
harbor run --configrejects.toml. Field names:AgentConfig.model_name(notmodel),VerifierConfig.disable(notenabled), noversion. Validate offline withJobConfig.model_validatebefore committing to a 15-min vLLM start. - Mount the agent runtime image (critical). OH-SDK needs Python ≥ 3.12 but
task images are
py<3.12, so the py3.12 runtime image must be mounted (docker.io/jierun/c-oh-sdk-1.14.0:v0.1→/opt/custom-agent-runtime/oh-sdk).KubernetesEnvironmentreadsagent_runtime_image/agent_runtime_mount_path/agent_runtime_image_subpath/host_path_mountsonly fromenvironment.kwargs— not fromHARBOR_*env vars (those were verl glue). Miss them and every trial fails withpython3 is < 3.12. - Dataset is a directory of harbor task dirs (
task.tomleach), pointed at by[[datasets]] path=; e.g./mnt/public/coding_rl/harbor_swe_val_tasks_official(500 tasks).