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:
loadModel()— load withmodelConfig.parallel >= 2.batchCompletion()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/mcprequire a tool-capable model loaded withmodelConfig.tools: true. - Per-prompt
attachmentsrequire a multimodal-capable model plus its matchingprojectionModelSrc— 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.eventsis a single async iterable yielding{ id, event }interleaved across all prompts, whereeventis the same discriminatedCompletionEvent(contentDelta,thinkingDelta,toolCall,completionStats,completionDone, etc.) used bycompletion(). - Per-prompt streams:
run.byId(id)returns{ events, final }scoped to one prompt — its own event iterable plus an aggregatedfinalpromise. - Assigned ids:
run.idsresolves to the addon-assigned ids in prompt order. - Ordered results:
run.resultsresolves toBatchCompletionResult[]in prompt order. It is all-or-nothing for stream-level failures — if the batch handler throws (e.g. aContextOverflowError), the whole run rejects and no per-prompt finals can be recovered. Graceful terminal events such as cancellation still settle eachbyId(id).finalaccording to that id's terminal state. - Batch-level stats:
run.statsresolves to a singleCompletionStats | undefinedaggregated across the whole batch (e.g. total tokens), rather than per prompt. - Per-prompt tools and MCP: each prompt can carry its own
toolsand/ormcpclients, 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 — 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.