LiteRT-LM reason-faithfulness bypass driver + fix native SIGTRAP on long generations#98
Merged
Merged
Conversation
…g generations Adds a standalone `:litert-eval` Gradle module that drives the on-device LiteRT-LM (Qwen3-0.6B-int4) generator over the golden Move Coach cases and emits raw outputs as JSON, to be scored by ferryman's reason-faithfulness scorer. Generation and scoring are deliberately decoupled (two repos, two languages, one JSON contract) — wiring LiteRT-LM into ferryman as a provider would be a multi-day feature for a 0.6B model that can't reliably do the MCP tool-call loop anyway. The module lives separately from `:evals` because litertlm-jvm 0.14.0 is an internally-inconsistent artifact: its bytecode calls `SendChannel.close$default` (a static bridge that only exists in kotlinx-coroutines 1.11.0+) while its POM declares 1.9.0. `:evals` pulls Ktor via `:server`, which drags coroutines to 1.10.2 — no version in that graph provides the bridge. `:litert-eval` depends on `:onDeviceAi` + `:coachapi` only and forces coroutines to 1.11.0, the single version that satisfies litertlm's bytecode. See `evals/docs/litert-faithfulness-bypass.md`. Also fixes a separate native crash that surfaced once the coroutines bridge worked: `LitertLmTextGenerator` requested `maxNumTokens = 2048` (DEFAULT_CONTEXT_SIZE) but the Qwen3 model's KV cache tops out at 1280 (`ekv1280` in the filename; `max_num_tokens: 1280` in its metadata). Compiles fine, but the litertlm-jvm native runtime traps with SIGTRAP (JVM exit 133) once a generation's prompt + Qwen3's `<think>` chain-of-thought + output crosses 1280 tokens — typically on checkmate cases (case 8 is 1.f3 e5 2.g4 Qh4#, Fool's Mate) that trigger long reasoning. The trap is a C++ CHECK, so it can't be caught by `try/catch (Throwable)` and kills the whole driver. Lower DEFAULT_CONTEXT_SIZE to 1024 (headroom under the 1280 cap). This also fixes the same latent crash in the production desktop Move Coach, which shares the generator. Fixture: normalize candidates.json tag vocabulary to the MoveCoachFallback constants (`develops`, `center-control`, ...) so the prompt hands the model translated phrases, not raw strings that miss `describeTags`' `when` branches. Verified: `./gradlew :litert-eval:run --args="10 litert-outputs.json"` completes all 10 cases (EXIT=0, was crashing at case 8 with exit 133).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a standalone
:litert-evalGradle module that drives the on-device LiteRT-LM (Qwen3-0.6B-int4) generator over the golden Move Coach cases and emits raw outputs as JSON, to be scored by ferryman's reason-faithfulness scorer. Generation and scoring are deliberately decoupled — two repos, two languages, one JSON contract.Also fixes a native crash (JVM exit 133 / SIGTRAP) that prevented the driver from completing.
The crash this fixes
./gradlew :litert-eval:run --args="10 litert-outputs.json"was dying at case 8:Exit 133 = 128 + 5 = SIGTRAP, a C++ assertion (
CHECKfailure) inside the litertlm-jvm native runtime — not a Kotlin exception, which is whytry/catch (Throwable)in the driver couldn't intercept it and the whole JVM died.Root cause:
LitertLmTextGeneratorrequestedmaxNumTokens = 2048(DEFAULT_CONTEXT_SIZE), but the Qwen3 model's KV cache tops out at 1280 (ekv1280in the filename;max_num_tokens: 1280in its metadata). Compiles fine, but the native runtime traps once a generation's prompt + Qwen3's<think>chain-of-thought + output crosses 1280 tokens — typically on checkmate cases. Case 8 is1.f3 e5 2.g4 Qh4#(Fool's Mate), whose mate explanation triggers long reasoning.Fix: lower
DEFAULT_CONTEXT_SIZEfrom 2048 → 1024 (headroom under the 1280 cap). This also fixes the same latent crash in the production desktop Move Coach, which sharesLitertLmTextGenerator.This is a different crash from the one already documented in
evals/docs/litert-faithfulness-bypass.md(theNoSuchMethodError: SendChannel.close$defaultcoroutines mismatch). That one is already fixed by theresolutionStrategy.forceto coroutines 1.11.0 inlitert-eval/build.gradle.kts— cases 1–7 were generating successfully, which proves the coroutines bridge works.Why a separate
:litert-evalmodulelitertlm-jvm 0.14.0 is internally inconsistent: its bytecode calls
SendChannel.close$default(a static bridge that only exists in kotlinx-coroutines 1.11.0+) while its POM declares 1.9.0.:evalspulls Ktor via:server, which drags coroutines to 1.10.2 — no version in that graph provides the bridge.:litert-evaldepends on:onDeviceAi+:coachapionly and forces coroutines to 1.11.0, the single version that satisfies litertlm's bytecode. Full rationale inevals/docs/litert-faithfulness-bypass.md.Fixture
Normalize
candidates.jsontag vocabulary to theMoveCoachFallbackconstants (develops,center-control, …) so the prompt hands the model translated phrases, not raw strings (development,center) that missdescribeTags'whenbranches.Verification
route=litertrecordsNext step (ferryman repo):
python3 eval_harness/score_litert_outputs.py /path/to/litert-outputs.json --write scorecard-litert.mdChanged files
litert-eval/— new module:build.gradle.kts(coroutines isolation + force),EvalLiteRtDriver.kt(the driver),.gitignore(excludes generatedlitert-outputs.json)onDeviceAi/.../LitertLmTextGenerator.kt—DEFAULT_CONTEXT_SIZE2048 → 1024 (the crash fix)evals/docs/litert-faithfulness-bypass.md— design docevals/golden/candidates.json— tag vocabulary normalizationsettings.gradle.kts— include:litert-eval