Kokoro int8 on Android/ARM: generate() intermittently returns an entire sentence as rail-pinned garbage (samples pinned at ~+1.0); fp32 unaffected
Environment
- sherpa-onnx 1.13.4 (Flutter package
sherpa_onnxfrom pub.dev; nativesherpa_onnx_android_arm641.13.4) - Pixel 10 Pro (arm64, Android 16), tested with
numThreads: 2and4 - Model: kokoro-int8-multi-lang-v1_0 (release tarball from this repo's
tts-modelstag) - Config:
OfflineTtsKokoroModelConfig(model: model.int8.onnx, voices: voices.bin, tokens: tokens.txt, dataDir: espeak-ng-data, lexicon: lexicon-us-en.txt, lang: en-us),maxNumSentences: 1,silenceScale: 0.2, CPU provider
Symptom
OfflineTts.generate() intermittently returns audio where an entire internal sentence chunk is rail-pinned garbage: every sample of that chunk is ≈ +1.0 (after our 16-bit encode, a constant +32767 for the chunk's full duration — e.g. a 2,030 ms pinned run at 5.08 s in a 7.11 s result). Played back, it's a loud pop followed by DC "silence" that swallows the sentence. The surrounding sentences in the same generate() call are normal speech.
Example failing input (sid=4 af_jessica, speed 1.0 — the second sentence comes back pinned):
He was holding a rifle in his hands — now they knew what had been in the long, thin package he had brought with them.
“Who’s there?” he shouted.Short texts fail too — “Who’s there?” alone, "Who's there?" (ASCII), he said., them. have all come back fully pinned in our logs.
Characterization
- Not input-deterministic — engine-state-dependent. The identical string (same sid/speed, same
OfflineTtsinstance lifecycle) rails or passes depending on what was synthesized before it. E.g.“Who’s there?”standalone railed in three separate app sessions at the same point of the same synthesis sequence, then passed at a different point of a different sequence;"Who's there?"railed at 22:44 and passed at 22:56 the same evening. A fixed sequence ofgenerate()calls reproduces the failure ~100% at the same call. - Strongly input-biased. With sid=4 (
af_jessica), short sentence-initial phrases (esp. quoted questions/exclamations) are hot: near-100% in our reading sequences. The same six test texts through sid=0 (af_alloy) in the same session: all clean. Embedding the hot phrase mid-sentence (He said “Who’s there?” again.) is clean — consistent with the corruption being per internal sentence chunk. - Happens at both
numThreads: 2and4. - x86_64 control: the same model files, same config values, same exact input bytes through the
sherpa-onnx1.13.4 Python wheel on x86_64 Windows — never reproduces (dozens of attempts, including the exact failing strings). - fp32 control:
kokoro-multi-lang-v1_0(fp32) on the same device, same config, same inputs, same sequences: zero failures across 15+ syntheses of the hottest int8-failing inputs plus full book-chapter runs.
The state-dependence + all-samples-pinned shape smells like an uninitialized/stale buffer read or a data race somewhere in the ARM int8 inference path, rather than a numeric overflow inside an otherwise-correct computation.
Two related observations on the int8 variant (same setup)
- int8 is slower than fp32 on this ARM device — int8 RTF ≈ 0.62 vs fp32 RTF ≈ 0.48–0.51 at 4 threads (same texts, same session). This mirrors #2374 (iPhone 15); it's not just Apple silicon.
- int8 output carries steady tones at exactly
sampleRate/5and2·sampleRate/5(4.8 kHz / 9.6 kHz at 24 kHz) — the iSTFT frame rate and its harmonic — measured 15–20 dB above the noise floor in inter-word gaps, audible as a high-pitch background whine. fp32 output is clean at those frequencies.
Given all three, we've switched our app from int8 to fp32, which resolves everything — filing this so the int8 artifacts either get fixed or come with a warning label.
Happy to provide captured WAVs (railed vs clean, device vs x86), full logs, or run instrumented builds on the device.
Source: k2-fsa/sherpa-onnx