QVAC Logo

Batch processing

Run multiple LLM prompts concurrently through a single loaded model in one call.

Overview

What it does: batchCompletion() runs multiple prompts through a single loaded model in one call, instead of firing a separate completion() call per prompt.

When to use it: you have several prompts to run against the same model and want them handled together as one job.

Why use it:

  • Optimizes resources: all prompts share one loaded model — you don't load the model multiple times to run prompts side by side.
  • Lower total run time: the prompts run together rather than one after another, so the whole set finishes faster than the equivalent sequence of completion() calls.

To use it, load the model with modelConfig.parallel >= 2 (this is what lets it handle more than one prompt at a time), then pass an array of prompts — each with its own history and, optionally, generationParams, responseFormat, tools, mcp, and multimodal attachments.

Batch processing is specific to LLM text generation. It does not apply to other capabilities such as embeddings or transcription.

Functions

Use the following sequence of function calls:

  1. loadModel() — load with modelConfig.parallel >= 2.
  2. batchCompletion()
  3. unloadModel()

For how to use each function, see SDK — API reference.

Models

You can load any llama.cpp-compatible text-generation/chat model (same models as Text generation). Model file format: *.gguf.

  • Per-prompt tools/mcp require a tool-capable model loaded with modelConfig.tools: true.
  • Per-prompt attachments require a multimodal-capable model plus its matching projectionModelSrc — see Multimodal.
  • For models available as constants, see SDK — Models.

Features

batchCompletion() returns a BatchCompletionRun that exposes several ways to consume the batch:

  • Merged event stream: run.events is a single async iterable yielding { id, event } interleaved across all prompts, where event is the same discriminated CompletionEvent (contentDelta, thinkingDelta, toolCall, completionStats, completionDone, etc.) used by completion().
  • Per-prompt streams: run.byId(id) returns { events, final } scoped to one prompt — its own event iterable plus an aggregated final promise.
  • Assigned ids: run.ids resolves to the addon-assigned ids in prompt order.
  • Ordered results: run.results resolves to BatchCompletionResult[] in prompt order. It is all-or-nothing for stream-level failures — if the batch handler throws (e.g. a ContextOverflowError), the whole run rejects and no per-prompt finals can be recovered. Graceful terminal events such as cancellation still settle each byId(id).final according to that id's terminal state.
  • Batch-level stats: run.stats resolves to a single CompletionStats | undefined aggregated across the whole batch (e.g. total tokens), rather than per prompt.
  • Per-prompt tools and MCP: each prompt can carry its own tools and/or mcp clients, resolved client-side (including MCP tool discovery) before submission. Tool calls are routed back to the correct prompt.
  • Cancellable: like any other request — see Cancellation.

Validation rules: prompt ids must be unique within a batch, and a prompt's responseFormat (json_object / json_schema) cannot be combined with tools or mcp (tools already constrain output via their parameter schema).

Example

The following script loads a model with parallel: 4, submits several prompts in one batchCompletion() call, consumes the merged event stream, and reads the ordered results and batch-level stats:

batch-completion.js
/**
 * Batch completion — continuous batching over a single loaded LLM.
 *
 * `batchCompletion()` submits multiple prompts to one loaded model in a single
 * call so the llama.cpp addon can decode the active sequences together. Load the
 * model with `modelConfig.parallel >= 2` to make the concurrent slots available.
 *
 * The returned `BatchCompletionRun` exposes:
 *  - `events`     — a merged `AsyncIterable<{ id, event }>` across all prompts.
 *  - `byId(id)`   — per-prompt `events` and a `final` aggregation for one prompt.
 *  - `ids`        — a `Promise<string[]>` of addon-assigned ids in prompt order.
 *  - `results`    — a `Promise<BatchCompletionResult[]>` in prompt order. It is
 *                   ALL-OR-NOTHING for stream-level failures: if the batch
 *                   handler throws (e.g. a context overflow) the whole run
 *                   rejects and no per-prompt finals can be recovered. Graceful
 *                   terminal events such as cancellation still settle each
 *                   `byId(id).final` according to that id's terminal state.
 *  - `stats`      — a `Promise<CompletionStats | undefined>`. Stats are
 *                   BATCH-LEVEL: the addon aggregates decode metrics across the
 *                   whole batch (e.g. `avgConcurrentSeq`, total tokens), so they
 *                   surface once here rather than per prompt. Per-prompt
 *                   `final.stats` is intentionally left undefined.
 *
 * Each prompt may additionally carry (not shown here to keep the run fast/cheap):
 *  - `tools` / `mcp`  — per-prompt tools; handlers attach only to tool calls
 *                       produced by that prompt. Requires a tool-capable model
 *                       loaded with `modelConfig.tools: true` (e.g. Qwen). See
 *                       the `tools/` examples for the tool-calling surface.
 *  - `attachments`    — the same multimodal attachments `completion()` supports,
 *                       when the loaded model has a projection model.
 *  - `responseFormat` — `json_object` / `json_schema` (cannot be combined with
 *                       `tools` or `mcp`, which already constrain output).
 *
 * Run from packages/sdk:
 *   bun run examples/batch-completion.ts
 */
import { batchCompletion, loadModel, unloadModel, LLAMA_3_2_1B_INST_Q4_0 } from '@qvac/sdk';
try {
    // `parallel: 4` opens the concurrent decode slots continuous batching needs.
    const modelId = await loadModel({
        modelSrc: LLAMA_3_2_1B_INST_Q4_0,
        modelType: 'llm',
        modelConfig: { ctx_size: 4096, parallel: 4 },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    const run = batchCompletion({
        modelId,
        prompts: [
            {
                id: 'cherry',
                history: [{ role: 'user', content: 'Reply with only the word CHERRY.' }],
                generationParams: { temp: 0, seed: 42, predict: 16 }
            },
            {
                id: 'banana',
                history: [{ role: 'user', content: 'Reply with only the word BANANA.' }],
                generationParams: { temp: 0, seed: 42, predict: 16 }
            },
            {
                id: 'grape',
                history: [{ role: 'user', content: 'Reply with only the word GRAPE.' }],
                generationParams: { temp: 0, seed: 42, predict: 16 }
            }
        ]
    });
    console.log('\n▸ Merged event stream (interleaved across prompts):');
    for await (const { id, event } of run.events) {
        if (event.type === 'contentDelta') {
            process.stdout.write(`[${id}] ${event.text}`);
        }
    }
    // `results` is ordered by prompt and all-or-nothing on stream-level failure.
    const results = await run.results;
    console.log('\n\n▸ Ordered results:');
    for (const { id, final } of results) {
        console.log(`  ▸ ${id}: ${final.contentText.replace(/\s+/g, ' ').trim()}`);
    }
    // Per-prompt aggregation is also available directly via `byId(id)`.
    const ids = await run.ids;
    const firstFinal = await run.byId(ids[0]).final;
    console.log(`\n▸ byId("${ids[0]}").final -> ${firstFinal.contentText.trim()}`);
    // Batch-level stats (a single CompletionStats for the whole run, or undefined).
    const stats = await run.stats;
    if (stats) {
        console.log(`\n▸ Batch stats: ${stats.tokensPerSecond?.toFixed(1)} tok/s across the batch`);
    }
    await unloadModel({ modelId, clearStorage: false });
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Tip: all examples throughout this documentation are self-contained and runnable. For instructions on how to run them, see SDK quickstart.

On this page

Ask anything about QVAC.