Repository navigation
[Recipe] Run CLM behind the frontend, and compare engine responses - #23
Conversation
hsliuustc0106
left a comment
There was a problem hiding this comment.
Reviewed commit d61c0d139621fd0f8941360009859c38c45b5e17.
[P2] Preserve success-status requirement in relaxed comparisons. recipe/compare_with_backend.py:90 sets ok=True for equal answer subtrees whenever statuses match, even when both statuses are 500. Reproduced all PASS and exit 0 with (500, application/json, {"answers":{},"detail":"failed"}). Require HTTP 200 in this fallback. Existing five tests pass but do not cover errors containing answers.
|
Fixed in fe319e1. The relaxed fallback now requires the worker's status to be 200 — since the same block already forces the two statuses equal, a matching pair of 500s carrying equal The error body on the test server is configurable, so the case the existing five did not cover is pinned by a sixth test. It fails against the previous revision. |
Levius-Fubuki
left a comment
There was a problem hiding this comment.
Reviewed fe319e16838a7d408ac330afae2d3ec9d3bb0780, including the complete recipe diff and the follow-up to the previous HTTP-status finding.
The original finding is fixed. An independent probe returning identical HTTP 500 JSON bodies with equal answers exits 0 on the previously reviewed revision and exits 1 on this head. The relaxed path now requires HTTP 200 and matching status/content-type. All six supplied HTTP comparison cases passed on Linux/Python 3.12; a first unittest-discovery invocation found no tests because this file uses its own runner, so validation used the documented python recipe/test_compare_with_backend.py command.
No new actionable findings. This is a COMMENT rather than merge approval: the branch currently conflicts with main. Please resolve those conflicts and rerun the comparison tests on the resulting head. I did not run CLM's full external server recipe or a GPU encoder for this recipe review.
|
fix conflicts |
fe319e1 to
9ddbca2
Compare
|
Rebased onto
|
hsliuustc0106
left a comment
There was a problem hiding this comment.
Independent review — verdict: ready to approve once rebased. The current head (9ddbca29) conflicts with main at 873655b: this PR and the just-merged #55 both add an entry to the recipe index, so recipe/README.md needs a one-line resolution. Everything below was verified at that head before main moved.
Verified locally (detached worktree, CPU only):
python3 recipe/test_compare_with_backend.py— 6/6 ok on Python 3.9.6- Upstream CLM's
src/clm/embedder.pyconfirms the three assumptions the recipe rests on: the client sends"encoding_format": "base64"(so the stub's base64 float32 responses are exactly what a real vLLM pooling server returns for this client), it health-checksGET /v1/models(the stub serves it), and its LRU cache reports "encoder tokens spent on cache misses" — precisely the field the comparator relaxation excludes, and why. - No stale references to
recipe/laya/compare_with_backend.pyremain; theREADME.md#compare-responsesanchor still resolves.
Findings (non-blocking):
- The relaxation compares the JSON-parsed
answerssubtree, so key-order/whitespace differences also pass, not justusage. Acceptable for a recipe tool (strict byte equality is tried first, so pure-function backends are unaffected) — noting it because the relaxation is slightly wider than "usage-only". stub_embedder.py:Handler.callsis incremented but never read — dead code.recipe/laya/apple-silicon.md:140still describescompare_with_backend.pyas "from the Laya text worker recipe" — stale after the move torecipe/.- The full three-terminal run (stub +
clm-serve+ frontend,X-CLM-Latency-Mspass-through) is author-reported and was not re-run here; the comparator logic itself is covered by the tests above.
Process note: after the rebase, this fork PR's workflow runs will be held in action_required again and will need maintainer approval to run.
CLM is the second model in ThinkFlowLab#9: a frozen Qwen3-8B encoder behind an OpenAI-compatible /v1/embeddings endpoint, two projection heads, and a cosine score. clm-serve is an HTTP client of that endpoint, so the whole path can run on CPU against a stub encoder -- no GPU, no vLLM, no 8B weights. That is what recipe/clm/ adds: the stub, the launch commands, and what the run shows. The frontend needs no adapter for it. Status, content type and all three answer types (choice, score, noul) pass through unchanged, including CLM's X-CLM-Latency-Ms header. The response comparison did need a change. It compared the whole body byte-for-byte, which holds for LAYA because its body is a pure function of the request. A backend that reuses encoder state across requests reports usage only for the calls it paid for, so two identical requests differ: same state, three times: noul=0.977197 usage.input_tokens=0 a state not seen before: noul=0.280477 usage.input_tokens=26 that same state again: noul=0.280477 usage.input_tokens=0 The decision is deterministic; input_tokens counts cache misses. Comparing the full body fails a correct response, and whether it fails depends on which call warmed the cache. The comparison now checks status, content type and the answers subtree, and prints the usage difference instead of asserting on it. Strict equality is still tried first, so a backend whose body is a pure function is unaffected. compare_with_backend.py and its test move to recipe/ because both recipes use them now; recipe/laya/README.md is updated for the new path. test_compare_with_backend.py covers the comparison with two throwaway HTTP servers and no model: identical responses pass, a usage-only difference passes and is reported, and a differing answer, a differing status and a non-200 backend all still fail. Verified against CLM with a cold cache, where the usage difference is real.
The comparison fell back to comparing the decision subtree whenever the two
statuses were equal and the answers matched, without requiring success — so
a worker and a frontend that both answered 500 with `{"answers": {}}` were
reported as PASS with exit 0.
Gate the fallback on the worker's status being 200. The status-equality
check inside the block then forces the frontend's to be 200 as well. The
test server's error body is now configurable so a regression test can pin
the case the existing five did not cover.
Found in review.
- Rebased on main; the recipe index conflict was this PR and ThinkFlowLab#55 both adding an entry, so both are kept. - The comparator's docstring said it compares body bytes. It does that first, and falls back to the parsed answers when only the envelope differs; the fallback also makes key order and whitespace irrelevant, which is wider than "usage only" and is now stated rather than implied. - stub_embedder.py counted requests in a class attribute nothing read. - apple-silicon.md still called compare_with_backend.py the Laya recipe's; it is shared now and lives in recipe/.
9ddbca2 to
2d89e92
Compare
- Rebased on main. The recipe index conflict was this PR and ThinkFlowLab#55 both adding an entry, so both are kept. - `CLM_CKPT_DIR` is not a variable `clm-serve` reads. It takes `CLM_CKPT` as the file path, and without it looks in `~/.cache/clm` and downloads -- so the command in this recipe would have quietly fetched a second copy of the checkpoint instead of using the one it names. - The comparator's docstring said it compares body bytes. It does that first and falls back to the parsed answers when only the envelope differs; that fallback also makes key order and whitespace irrelevant, which is wider than "usage only" and is now stated rather than implied. - `stub_embedder.py` counted requests in a class attribute nothing read. - `apple-silicon.md` still called `compare_with_backend.py` the Laya recipe's; it is shared now and lives in `recipe/`.
|
Rebased onto Reran the three-terminal setup end to end on this head — stub embedder, The three answer types come back through the frontend unchanged, including the Also corrected, from your findings and one more found while running it:
|
|
lgtm |
CLM is the second model in #9: a frozen Qwen3-8B encoder behind an OpenAI-compatible
/v1/embeddingsendpoint, two projection heads, and a cosine score.clm-serveis an HTTP client of that endpoint, so the whole path runs on CPU against a stub encoder — no GPU, no vLLM, no 8B weights.recipe/clm/has the stub, the launch commands and what the run shows. Point it at the real encoder withserve_qwen3_8b.shand nothing downstream changes; that is the property worth having a recipe for.The frontend needs no adapter. Status, content type and all three answer types pass through unchanged, including CLM's
X-CLM-Latency-Ms:The response comparison did need a change. It compared the whole body byte-for-byte, which holds for LAYA because its body is a pure function of the request. A backend that reuses encoder state across requests reports usage only for the calls it paid for, so two identical requests differ:
The decision is deterministic;
input_tokenscounts cache misses. Comparing the full body fails a correct response, and whether it fails depends on which call happened to warm the cache — so the same run passes or fails on ordering. The comparison now checks status, content type and theanswerssubtree, and prints the usage difference rather than asserting on it. Strict equality is still tried first, so a backend whose body is a pure function is unaffected. The relaxation applies only to a successful status: it used to accept any two equal statuses, so a matching pair of 500s carrying an equalanswerssubtree passed.compare_with_backend.pyand its test move torecipe/because both recipes use them now.Tests
recipe/test_compare_with_backend.pyruns two throwaway HTTP servers, no model and no Rust binary:Also verified against CLM with a cold cache, where the usage difference is real rather than zero on both sides.
Open question
Which response fields may differ between two otherwise identical requests?
billing_unitslooks stable,input_tokensdoes not. If the project wants the strong form — the whole body identical — theninput_tokenshas to mean "tokens the request required" rather than "tokens this call paid for", which is a decision for the engine, not the frontend.