Skip to content

LiteRT-LM reason-faithfulness bypass driver + fix native SIGTRAP on long generations#98

Merged
ber4444 merged 1 commit into
mainfrom
eval/litert-faithfulness-bypass
Jul 17, 2026
Merged

LiteRT-LM reason-faithfulness bypass driver + fix native SIGTRAP on long generations#98
ber4444 merged 1 commit into
mainfrom
eval/litert-faithfulness-bypass

Conversation

@ber4444

@ber4444 ber4444 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

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.

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:

> Process '.../bin/java' finished with non-zero exit value 133
  (this value may indicate that the process was terminated with the SIGTRAP signal)

Exit 133 = 128 + 5 = SIGTRAP, a C++ assertion (CHECK failure) inside the litertlm-jvm native runtime — not a Kotlin exception, which is why try/catch (Throwable) in the driver couldn't intercept it and the whole JVM died.

Root cause: 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 native runtime traps 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), whose mate explanation triggers long reasoning.

Fix: lower DEFAULT_CONTEXT_SIZE from 2048 → 1024 (headroom under the 1280 cap). This also fixes the same latent crash in the production desktop Move Coach, which shares LitertLmTextGenerator.

Note for reviewers: the context-size change affects the live desktop Move Coach feature, not just the eval driver. The old value (2048) was unachievable against this model — the model can only honour 1280 — so it was a latent crash in the live feature too, not just the eval.

This is a different crash from the one already documented in evals/docs/litert-faithfulness-bypass.md (the NoSuchMethodError: SendChannel.close$default coroutines mismatch). That one is already fixed by the resolutionStrategy.force to coroutines 1.11.0 in litert-eval/build.gradle.kts — cases 1–7 were generating successfully, which proves the coroutines bridge works.

Why a separate :litert-eval module

litertlm-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. :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. Full rationale in evals/docs/litert-faithfulness-bypass.md.

Fixture

Normalize candidates.json tag vocabulary to the MoveCoachFallback constants (develops, center-control, …) so the prompt hands the model translated phrases, not raw strings (development, center) that miss describeTags' when branches.

Verification

./gradlew :litert-eval:run --args="10 litert-outputs.json"
before after
result exit 133 (SIGTRAP) at case 8 EXIT=0, 10/10 cases
cases completed 7 10
route=litert records 0 (file not written) 10

Next step (ferryman repo): python3 eval_harness/score_litert_outputs.py /path/to/litert-outputs.json --write scorecard-litert.md

Changed files

  • litert-eval/ — new module: build.gradle.kts (coroutines isolation + force), EvalLiteRtDriver.kt (the driver), .gitignore (excludes generated litert-outputs.json)
  • onDeviceAi/.../LitertLmTextGenerator.ktDEFAULT_CONTEXT_SIZE 2048 → 1024 (the crash fix)
  • evals/docs/litert-faithfulness-bypass.md — design doc
  • evals/golden/candidates.json — tag vocabulary normalization
  • settings.gradle.kts — include :litert-eval

…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).
@ber4444
ber4444 merged commit 7a1ea0c into main Jul 17, 2026
12 checks passed
@ber4444
ber4444 deleted the eval/litert-faithfulness-bypass branch July 17, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant