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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions