Hi Polar team, thanks for the paper and the open reference examples.
I hit an issue reproducing §4.1 GRPO training with the polar_bridge
plus slime combination, and the empirical evidence points at
sglang-router silently stripping the extended response fields that
SGLangEngine.normalize_response relies on. I want to check whether
you saw the same thing internally and, if not, what your actual
setup differed on.
My setup
- Polar (this repo) commit: the latest stable branch
- Slime:
0.3.1 (from slime-main at HEAD)
- sglang:
0.5.15.post1
- sglang-router: swept
0.1.0 through 0.3.2 (see bisection below)
- Model:
Qwen2.5-7B-Instruct (local HF snapshot)
- Hardware: 4 x L20X 140 GB, single node
- Config: adapted from
examples/swegym_slime_grpo/run.sh for a
HumanEval+MBPP task set; the polar-specific wiring is byte-for-byte
the same
(--rollout-function-path slime_bridge.rollout.generate_rollout_polar_async,
default SGLANG_ROUTER_BASE_URL=http://<host>:9000, i.e. slime's
managed sglang-router).
Symptom
Every rollout group is dropped by the slime bridge:
RolloutManager: Dropping Polar group N because of zero trainable tokens
adapter.py:71 Session ...: no usable trace (traces=K, max_tokens=60000)
Session-side everything looks healthy (claude_code CLI ran multi-turn
tool loops, gateway logs show 200 OK on /v1/messages, session
COMPLETED, trace count > 0, response messages populated). Every
resulting Trace has prompt_ids=[] and response_ids=[].
Root cause (empirically pinned)
Slime's sglang_router.launch_router deserializes upstream sglang
responses into strict Rust structs modeled on OpenAI's Chat
Completions schema (ChatCompletionResponse, ChatChoice). sglang's
extension fields prompt_token_ids (top of choice) and meta_info
(carrying output_token_logprobs etc.) are not in that schema, so
Rust serde silently drops them during the deserialize + reserialize
round trip.
Polar's SGLangEngine.normalize_response reconstructs both
trace.prompt_ids and trace.response_ids from those two fields.
Once the router strips them, _canonicalize_prompt_token_ids and
_canonicalize_response_token_ids produce nothing, and all three
fallbacks in record_utils.py::_extract_response_tokens return
None (there is no token_id in logprobs.content[] on either side
of the router, so the OpenAI-standard logprobs fallback also fails).
Empirical probe (5 minutes to reproduce)
# 1. Launch sglang engine on port 30000
python3 -m sglang.launch_server --model-path <snapshot> \
--served-model-name Qwen2.5-7B-Instruct \
--host 127.0.0.1 --port 30000 --tp 1
# 2. Launch sglang-router on port 30080 pointing at the engine
python3 -m sglang_router.launch_router \
--host 127.0.0.1 --port 30080 \
--worker-urls http://127.0.0.1:30000
# 3. Same request body sent to both endpoints
BODY='{"model":"Qwen2.5-7B-Instruct",
"messages":[{"role":"user","content":"Say hi in 5 words"}],
"max_tokens":12,"logprobs":true,"top_logprobs":0,
"return_prompt_token_ids":true,"return_meta_info":true}'
curl -s http://127.0.0.1:30000/v1/chat/completions \
-H 'Content-Type: application/json' -d "$BODY" \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(sorted(d['choices'][0]))"
curl -s http://127.0.0.1:30080/v1/chat/completions \
-H 'Content-Type: application/json' -d "$BODY" \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(sorted(d['choices'][0]))"
Output:
direct: ['finish_reason', 'index', 'logprobs', 'matched_stop', 'message', 'meta_info', 'prompt_token_ids']
router: ['finish_reason', 'index', 'logprobs', 'matched_stop', 'message']
Response body size: direct 2079 bytes, router 1191 bytes (43% drop).
Version bisection (sglang-router)
I swept every reachable version to find when the strip was
introduced:
| version |
usable? |
strips prompt_token_ids and meta_info? |
0.1.0 through 0.1.4 |
✅ |
no (passthrough) |
0.1.5 |
✅ |
yes (regression introduced here) |
0.1.9 |
✅ |
yes |
0.2.0, 0.2.2, 0.2.3 |
❌ worker-registration path changed; /v1/models not routable with the standard launch args |
n/a |
0.2.4 |
✅ |
yes |
0.3.0 |
✅ |
yes |
0.3.1 |
✅ |
yes |
0.3.2 (current pypi latest) |
✅ |
yes |
Strip has been present since 0.1.5, well before slime==0.3.1's
sglang-router>=0.3.0 constraint could be satisfied. Any run that
respects slime's own dependency floor will hit it.
Workaround we shipped
Bypass the router: point SGLANG_ROUTER_BASE_URL at slime's rollout
engine on :15000 directly rather than at slime's router on :9000
(with ROLLOUT_NUM_GPUS=1 ROLLOUT_NUM_GPUS_PER_ENGINE=1, the engine
port is predictable via
_allocate_rollout_engine_addr_and_ports_normal). Trades away
router load balancing and cache awareness at multi-engine scale, but
recovers prompt_ids and response_ids and lets GRPO advance. Nine
consecutive training steps clean, rollout_success_rate=1.0 every
batch.
Questions
- What sglang-router version was in use for the §4.1 SWE-Gym GRPO
experiments? examples/swegym_slime_grpo/run.sh defaults
SGLANG_ROUTER_BASE_URL to :9000 (through the router). If the
run was on 0.1.4 or older, that would explain why you saw nonzero
rewards where we saw all-drop.
- Was
SGLANG_ROUTER_BASE_URL overridden in your actual run to
point directly at the engine (i.e. was the committed default not
what you actually used)? Our current fix is effectively this
override.
- Or is there a third path we are missing? e.g. a different
SGLangEngine implementation that reads tokens from somewhere
other than prompt_token_ids and
meta_info.output_token_logprobs, a slime-side hook that captures
token IDs before the response passes through the router, or a
variant of polar_bridge.rollout not in this repo.
- Do you plan to add an upstream fix or a workaround in
SGLangEngine? Two options seem reasonable:
- Router side (upstream
sgl-project/sglang): add
#[serde(flatten)] extra: HashMap<String, serde_json::Value> on
ChatCompletionResponse, ChatChoice, ChatMessage, Delta,
Usage, LogProbs so unknown fields survive the deserialize +
reserialize round trip. Or a --forward-extra-fields flag with
the same effect. Cleanest fix but requires an upstream release.
- Polar side (this repo): either document the "point at engine
directly" workaround in the reference example, or make
SGLangEngine.prepare_request request a backend flag that emits
token_id inside logprobs.content[] entries (sglang does not
natively populate logprobs.content[i].token_id; I verified
this empirically), so that the existing
_paired_tokens_from_logprobs_content fallback becomes viable
without meta_info.
Happy to test any patch or config. Repro scripts and full response
dumps available on request. Thanks for reading.
Hi Polar team, thanks for the paper and the open reference examples.
I hit an issue reproducing §4.1 GRPO training with the polar_bridge
plus slime combination, and the empirical evidence points at
sglang-router silently stripping the extended response fields that
SGLangEngine.normalize_responserelies on. I want to check whetheryou saw the same thing internally and, if not, what your actual
setup differed on.
My setup
0.3.1(fromslime-mainat HEAD)0.5.15.post10.1.0through0.3.2(see bisection below)Qwen2.5-7B-Instruct(local HF snapshot)examples/swegym_slime_grpo/run.shfor aHumanEval+MBPP task set; the polar-specific wiring is byte-for-byte
the same
(
--rollout-function-path slime_bridge.rollout.generate_rollout_polar_async,default
SGLANG_ROUTER_BASE_URL=http://<host>:9000, i.e. slime'smanaged sglang-router).
Symptom
Every rollout group is dropped by the slime bridge:
Session-side everything looks healthy (claude_code CLI ran multi-turn
tool loops, gateway logs show 200 OK on
/v1/messages, sessionCOMPLETED, trace count > 0, response messages populated). Every
resulting
Tracehasprompt_ids=[]andresponse_ids=[].Root cause (empirically pinned)
Slime's
sglang_router.launch_routerdeserializes upstream sglangresponses into strict Rust structs modeled on OpenAI's Chat
Completions schema (
ChatCompletionResponse,ChatChoice). sglang'sextension fields
prompt_token_ids(top of choice) andmeta_info(carrying
output_token_logprobsetc.) are not in that schema, soRust
serdesilently drops them during the deserialize + reserializeround trip.
Polar's
SGLangEngine.normalize_responsereconstructs bothtrace.prompt_idsandtrace.response_idsfrom those two fields.Once the router strips them,
_canonicalize_prompt_token_idsand_canonicalize_response_token_idsproduce nothing, and all threefallbacks in
record_utils.py::_extract_response_tokensreturnNone(there is notoken_idinlogprobs.content[]on either sideof the router, so the OpenAI-standard logprobs fallback also fails).
Empirical probe (5 minutes to reproduce)
Output:
Response body size: direct 2079 bytes, router 1191 bytes (43% drop).
Version bisection (sglang-router)
I swept every reachable version to find when the strip was
introduced:
prompt_token_idsandmeta_info?0.1.0through0.1.40.1.50.1.90.2.0,0.2.2,0.2.3/v1/modelsnot routable with the standard launch args0.2.40.3.00.3.10.3.2(current pypi latest)Strip has been present since 0.1.5, well before
slime==0.3.1'ssglang-router>=0.3.0constraint could be satisfied. Any run thatrespects slime's own dependency floor will hit it.
Workaround we shipped
Bypass the router: point
SGLANG_ROUTER_BASE_URLat slime's rolloutengine on
:15000directly rather than at slime's router on:9000(with
ROLLOUT_NUM_GPUS=1 ROLLOUT_NUM_GPUS_PER_ENGINE=1, the engineport is predictable via
_allocate_rollout_engine_addr_and_ports_normal). Trades awayrouter load balancing and cache awareness at multi-engine scale, but
recovers
prompt_idsandresponse_idsand lets GRPO advance. Nineconsecutive training steps clean,
rollout_success_rate=1.0everybatch.
Questions
experiments?
examples/swegym_slime_grpo/run.shdefaultsSGLANG_ROUTER_BASE_URLto:9000(through the router). If therun was on 0.1.4 or older, that would explain why you saw nonzero
rewards where we saw all-drop.
SGLANG_ROUTER_BASE_URLoverridden in your actual run topoint directly at the engine (i.e. was the committed default not
what you actually used)? Our current fix is effectively this
override.
SGLangEngineimplementation that reads tokens from somewhereother than
prompt_token_idsandmeta_info.output_token_logprobs, a slime-side hook that capturestoken IDs before the response passes through the router, or a
variant of
polar_bridge.rolloutnot in this repo.SGLangEngine? Two options seem reasonable:sgl-project/sglang): add#[serde(flatten)] extra: HashMap<String, serde_json::Value>onChatCompletionResponse,ChatChoice,ChatMessage,Delta,Usage,LogProbsso unknown fields survive the deserialize +reserialize round trip. Or a
--forward-extra-fieldsflag withthe same effect. Cleanest fix but requires an upstream release.
directly" workaround in the reference example, or make
SGLangEngine.prepare_requestrequest a backend flag that emitstoken_idinsidelogprobs.content[]entries (sglang does notnatively populate
logprobs.content[i].token_id; I verifiedthis empirically), so that the existing
_paired_tokens_from_logprobs_contentfallback becomes viablewithout
meta_info.Happy to test any patch or config. Repro scripts and full response
dumps available on request. Thanks for reading.