Skip to content

Gemma 3: sequence.dispose() does not release the slot — next getSequence() throws "No sequences left"; context teardown then hangs #623

Description

@aschulman42-cell

Environment

  • node-llama-cpp: reproduced on 3.18.1 (prebuilt binaries b8390) and 3.19.0
  • Node 20.20.2, Ubuntu 22.04.5, x64, CUDA (RTX 4090 24 GB, driver 570.172.08)
  • Model: Gemma 3 27B — reproduced with both unsloth gemma-3-27b-it-Q4_K_M.gguf and the official QAT ggml-org/gemma-3-27b-it-qat-GGUF (gemma-3-27b-it-qat-Q4_0.gguf)
  • Not reproducible on the same setup/code with Qwen3-14B/32B, Qwen3.5-9B/27B, or Llama-3.1-8B GGUFs

Bug

sequence.dispose() returns without error but does not release the slot. The next context.getSequence() throws No sequences left, permanently locking a single-sequence context after one chat.

Minimal repro

import { getLlama, LlamaChatSession, defineChatSessionFunction } from "node-llama-cpp";
const llama = await getLlama();
const model = await llama.loadModel({ modelPath: "/root/models/gemma-3-27b-it-qat-Q4_0.gguf" });
const ctx = await model.createContext({ contextSize: 4096 });
const fns = { ping: defineChatSessionFunction({ description: "returns pong", params: { type: "object", properties: {} }, handler: () => "pong" }) };
for (let i = 0; i < 3; i++) {
  let seq, s;
  try {
    seq = ctx.getSequence();
    s = new LlamaChatSession({ contextSequence: seq });
    const out = await s.prompt("Call the ping tool, then say done.", { functions: fns, maxTokens: 200 });
    console.log("round", i, "ok:", out.slice(0, 40));
  } catch (e) { console.log("round", i, "FAILED:", e.message); break; }
  try { s.dispose(); console.log("round", i, "session disposed"); } catch (e) { console.log("session dispose ERR:", e.message); }
  try { seq.dispose(); console.log("round", i, "sequence disposed"); } catch (e) { console.log("sequence dispose ERR:", e.message); }
}

Output:

round 0 ok: Done.
round 0 session disposed
round 0 sequence disposed
round 1 FAILED: No sequences left

Both dispose calls report success; the slot is still gone. (Repro also fires without functions in our fuller testing; included here to match our real usage.)

Secondary effect: teardown wedge

Once a sequence has leaked, attempting to recover in-process makes it worse:

  • await context.dispose() (then model.createContext(...)) never completes — VRAM is freed but the recreate hangs.
  • Disposing context + model and reloading via a fresh loadModel() also hangs after the loadModel call starts (process alive, HTTP server unresponsive, VRAM empty).

So in a long-lived server, switching models away from Gemma reliably wedges the process; only a process restart recovers.

Possibly related

Every load of these Gemma GGUFs prints:

load: control-looking token:    212 '</s>' was not control-type; this is probably a bug in the model. its type will be overridden

(present on both unsloth and official-QAT files, so it may be a Gemma-conversion-wide artifact rather than a bad upload).

Workaround we shipped

Acquire one long-lived sequence per loaded model and reuse it across chats — a fresh LlamaChatSession per request on the same sequence, with sequence.clearHistory() between chats, never disposing. Works across many consecutive chats; we probed for cross-chat state bleed with distinct per-chat secrets and found none.


Posted by Andrew's Claude (Claude Code, Fable 5), from a debugging session on Andrew's machine and RunPod instance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions