jingyaogong/minimind · critical · RuntimeError

SGLang update_policy failed

Error message

SGLang update_policy failed

What it means

RuntimeError raised by SGLangRolloutEngine.update_policy after the training process saves updated weights (save_pretrained to the shared checkpoint dir, half precision, safe_serialization=False) and then POSTs to the SGLang server's /update_weights_from_disk endpoint. It fires when the POST returns non-200 (printed as '[SGLANG WARNING] update_weights failed: <code>, <body>') or throws (printed as 'update_weights exception'), and the failure flag has been broadcast from rank 0 to all distributed ranks via dist.broadcast, so every rank raises together. It is a hard stop: the RL loop (PPO/GRPO-style weight sync) cannot continue with stale policy weights.

Source

Thrown at trainer/rollout_engine.py:193

    def update_policy(self, model: torch.nn.Module):
        ok = True
        if not dist.is_initialized() or dist.get_rank() == 0:
            try:
                unwrapped = model.module if isinstance(model, DistributedDataParallel) else model
                unwrapped = getattr(unwrapped, '_orig_mod', unwrapped)
                abs_path = os.path.abspath(self.shared_ckpt_path)
                state_dict = {k: v.detach().half().cpu() for k, v in unwrapped.state_dict().items()}
                unwrapped.save_pretrained(abs_path, state_dict=state_dict, safe_serialization=False)
                self.tokenizer.save_pretrained(abs_path)
                resp = self.http.post(f"{self.base_url}/update_weights_from_disk", json={"model_path": abs_path}, timeout=self.timeout)
                if resp.status_code != 200: print(f"[SGLANG WARNING] update_weights 失败: {resp.status_code}, {resp.text}")
                ok = resp.status_code == 200
            except Exception as e:
                print(f"[SGLANG WARNING] update_weights 异常: {e}"); ok = False
        if dist.is_initialized():
            ok_t = torch.tensor(int(ok), device=next(model.parameters()).device)
            dist.broadcast(ok_t, src=0); dist.barrier(); ok = bool(ok_t.item())
        if not ok: raise RuntimeError("SGLang update_policy failed")
        return ok
    
    def flush_cache(self) -> bool:
        resp = self.http.post(f"{self.base_url}/flush_cache", timeout=30)
        return resp.status_code == 200
    
    def health(self) -> bool:
        try:
            resp = self.http.get(f"{self.base_url}/health", timeout=5)
            return resp.status_code == 200
        except:
            return False


# ===== 工厂函数 =====
def create_rollout_engine(
    engine_type: str = "torch",
    policy_model: torch.nn.Module = None,

View on GitHub (pinned to 393e387e9a)

Solutions

  1. Check the printed WARNING line immediately above the raise — it contains resp.status_code and resp.text from SGLang, which names the real cause.
  2. Confirm the SGLang server is alive: engine.health() (GET {base_url}/health) and that base_url is reachable from the trainer process.
  3. Make sglang_shared_path truly shared and writable by both processes (same host dir, or correctly mounted volume in both containers) and verify with os.path.exists after save.
  4. Retry the sync once after flush_cache(): stale KV cache or a busy server can reject the update; POST /flush_cache then re-run update_policy.
  5. Bump self.timeout — loading large half-precision weights from disk can exceed a short HTTP timeout.
  6. Upgrade/align SGLang version so /update_weights_from_disk is supported, and prefer safe_serialization=True if the server requires safetensors.
  7. Write atomically (save to temp dir, os.replace/rename to final path) so the server never loads a half-written checkpoint.

Example fix

# before
resp = self.http.post(f"{self.base_url}/update_weights_from_disk", json={"model_path": abs_path}, timeout=self.timeout)
if resp.status_code != 200: print(f"[SGLANG WARNING] update_weights failed: {resp.status_code}, {resp.text}")
ok = resp.status_code == 200

# after
tmp_path = abs_path + '.tmp'
unwrapped.save_pretrained(tmp_path, state_dict=state_dict, safe_serialization=False)
self.tokenizer.save_pretrained(tmp_path)
if os.path.exists(abs_path): shutil.rmtree(abs_path)
os.rename(tmp_path, abs_path)
resp = self.http.post(f"{self.base_url}/update_weights_from_disk", json={"model_path": abs_path}, timeout=self.timeout)
ok = resp.status_code == 200
if not ok:
    self.flush_cache()
    resp = self.http.post(f"{self.base_url}/update_weights_from_disk", json={"model_path": abs_path}, timeout=self.timeout)
    ok = resp.status_code == 200
Defensive patterns

Strategy: try-catch

Validate before calling

# Pre-flight before update_policy(): server up + shared path visible + writable
assert engine.health(), 'SGLang server not healthy'
shared = os.path.abspath(engine.shared_ckpt_path)
assert os.path.isdir(shared), f'shared path missing on trainer: {shared}'
probe = os.path.join(shared, '.probe')
with open(probe, 'w') as f: f.write('x')
os.remove(probe)  # trainer side writable; confirm same volume is mounted in the server container too

Try / catch

try:
    ok = engine.update_policy()
except RuntimeError as e:
    if 'SGLang update_policy failed' in str(e):
        # the real cause was printed as '[SGLANG WARNING] update_weights ...' on rank 0;
        # flush stale cache and retry once before aborting the RL step
        engine.flush_cache()
        ok = engine.update_policy()
        if not ok:
            raise
    else:
        raise

Prevention

When it happens

Trigger: Calling update_policy() while training multi-process (dist.is_initialized) after a gradient step: rank 0 writes the checkpoint to sglang_shared_path and calls POST {base_url}/update_weights_from_disk with json {'model_path': abs_path}. It fails when the SGLang server is down/unreachable, when the path is not visible to the server process (different mount/container), when save_pretrained partially wrote (no atomic rename), when the server rejects the non-safetensors pickle format, or on update timeout (self.timeout). Non-200 prints a WARNING with resp.text; exceptions print the exception; both set ok=False which is broadcast, then raise.

Common situations: SGLang server and trainer in separate containers/NFS mounts where the 'shared' checkpoint dir is not actually shared; SGLang version that lacks or renamed /update_weights_from_disk; disk full or permission error while writing the checkpoint; race where the server reads weights mid-write because safe_serialization=False writes a pickle non-atomically; timeout too small for large models; rank>0 processes raising because rank 0 failed and the broadcast propagated ok=0.


AI-assisted analysis of jingyaogong/minimind@393e387e9a (2026-08-15). Data as JSON: /api/errors/a5532e118ab735a6. Report an issue: GitHub.