pinecallDocs
Operating / Your own GPU

Your own GPU

Open models on your hardware as the runtime's vendors: no cloud in the call.

The runtime names no vendor in code: every livekit plugin it carries is a vendor, and the providers row says which ones a call runs on. So open models on your own NVIDIA GPU can be the runtime's vendors with nothing but data: three model servers, and a row that points at them. No cloud vendor hears, thinks or speaks in the call, and no vendor bills for it.

How it works#

Every server speaks the OpenAI API, so the runtime reaches all of them through the openai plugin it already has, with a base_url per stage.

stage model server how the runtime reaches it
ears Whisper large-v3-turbo (99 languages) Speaches the openai plugin, base_url in the row
the end of a turn Smart Turn v3 (8 MB, 23 languages) the worker's own CPU turn_model in the row
thinking Google Gemma 4 12B Ollama the openai plugin, base_url in the row
voice Kokoro-82M Kokoro-FastAPI the openai plugin, base_url in the row
memory and knowledge bge-m3 (1024 wide) Ollama the row's embedding

The servers run with Docker Compose on the machine that holds the card, outside the cluster. The cluster's pods reach them over a private network: the workers for a call's ears, model and voice, and the gateways for the embedder and the hang-up judges. Both files are in the runtime's infra/models/.

What you need#

  • A runtime, on Kubernetes or on your laptop, and its operator key (PINECALL_OPS_KEY; on a cluster the secret pinecall-<env>-ops-key in Secret Manager).
  • An NVIDIA card with 12 GB of memory or more. Everything loaded takes about 10 GB: Gemma about 8.5, the ears and Kokoro about 1 each. It was measured on 24 GB (an RTX 3090).
  • About 25 GB of disk for the images and the models.
  • The NVIDIA driver, Docker with the compose plugin and NVIDIA's container toolkit on that machine. nvidia-smi shows the card, and docker run --rm --gpus all ubuntu nvidia-smi shows it from a container.
  • A private route from the cluster to the machine on ports 11434, 8880 and 8000. The repository's Terraform makes neither the GPU machine nor a firewall rule for it: make both the way your network is run, and admit the cluster's pod and node ranges alone. Never a public address: the servers ask for no key, so anyone who reaches them uses your GPU.

Set it up#

Start the servers#

On the machine with the card, with MODELS_BIND the private address the cluster reaches it at:

Terminal
mkdir ~/pinecall-models && cd ~/pinecall-models
curl -fsSLO https://raw.githubusercontent.com/pinecall/runtime/main/infra/models/compose.yaml
curl -fsSLO https://raw.githubusercontent.com/pinecall/runtime/main/infra/models/providers.json
MODELS_BIND=10.0.0.5 docker compose up -d

The first start is slow, and only the first: Ollama pulls Gemma 4 (7.6 GB) and bge-m3, and the ears pull Whisper (1.6 GB). The volumes keep them. docker compose ps -a shows ollama-models and whisper-model exited (0) once the models are in.

Check each server#

Terminal
H=10.0.0.5
curl -s http://$H:11434/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"gemma4:12b","reasoning_effort":"none","messages":[{"role":"user","content":"Di hola"}]}'
curl -s http://$H:8880/v1/audio/speech -H 'Content-Type: application/json' \
  -d '{"model":"kokoro","voice":"ef_dora","input":"Hola, ¿en qué te puedo ayudar?","response_format":"wav"}' -o hola.wav
curl -s http://$H:8000/v1/audio/transcriptions -F file=@hola.wav \
  -F model=deepdml/faster-whisper-large-v3-turbo-ct2 -F language=es
curl -s http://$H:11434/v1/embeddings -H 'Content-Type: application/json' \
  -d '{"model":"bge-m3","input":"hola"}' | head -c 120

A word back from the model, a WAV you can play, that WAV written back as text, and 1024 numbers.

Point the runtime at them#

Put the machine's address in the row: sed 's/MODELS_HOST/10.0.0.5/g' providers.json > row.json. A runtime that serves calls already has a row, and PUT /v1/ops/providers replaces it whole, so read it first (GET /v1/ops/providers), merge the open stack's defaults, voices, tuning, rates, judge and embedding into it, and send the result:

Terminal
curl -fsS -X PUT https://<your name>/v1/ops/providers \
  -H "Authorization: Bearer $PINECALL_OPS_KEY" -H 'Content-Type: application/json' --data @row.json

A runtime with no row yet, such as a new laptop, takes the file as it is: pinecall-runtime providers seed row.json.

Give the vendor a key#

The servers take no key, but the plugin refuses an empty one, so the runtime holds a placeholder for openai:

Terminal
curl -fsS -X PUT https://<your name>/v1/ops/provider-keys/openai \
  -H "Authorization: Bearer $PINECALL_OPS_KEY" -H 'Content-Type: application/json' -d '{"key": "local"}'

An org runs on it where its quota lends openai (Limits and provider keys).

The row's tuning sends every openai stage to your servers, and the placeholder replaces the runtime's own OpenAI key. A runtime that also runs OpenAI's models stops reaching them. And a new embedding leaves the facts and chunks written under the old one findable by their words only, until they are written again (pinecall-runtime memory reembed does it for memory).

A call#

The smallest agent that uses all three stages is one class with its prompt in the docstring:

agents/recepcion/agent.tsx
import { Agent } from "@pinecall/agents";

/**
 * Eres la recepción de la peluquería Rulos, en Montevideo. Hablas de vos, con frases cortas.
 * Todo lo que dices se lee en voz alta: sin listas, sin markdown, los números como se dicen.
 * Abrimos de martes a sábado, de diez de la mañana a siete de la tarde. El corte cuesta ochocientos pesos.
 */
export default class Recepcion extends Agent {}

Run pinecall start in the project, open the agent in the console's sandbox and talk to it. pinecall sessions show <call> then shows what the ears heard, what the model said and how long each stage took.

What the row says#

field what it does
stt/openai Whisper does not stream: the session's voice detector cuts each sentence and the whole sentence is transcribed at once. use_realtime: false keeps the plugin on that endpoint
voices the voice per language, openai/<language>. Kokoro's Spanish voices are ef_dora, em_alex and em_santa; curl http://$H:8880/v1/audio/voices lists them all
turn_model: smart-turn-v3 how the end of the caller's turn is read. Left out, it is livekit's own v1-mini. Measure both on your agents and keep the better
reasoning_effort: none Gemma 4 thinks before it answers unless told not to; on a phone that is a second of silence
rates at zero a call on your GPU costs nothing a vendor bills, and the console's cost columns read zero
judge the hang-up judges run on the same Gemma

When something does not come up#

what you see why what to do
could not select device driver "nvidia" the container toolkit is not configured for Docker configure it for Docker (nvidia-ctk runtime configure --runtime=docker), then restart Docker
the ears answer 404 for the model the model is not downloaded yet wait for whisper-model to exit (0), or docker compose up whisper-model
the first turn of a call waits ~7 s Speaches unloaded an idle Whisper the compose keeps it (WHISPER__TTL=-1) and loads it at start
the first answer after a while takes ~20 s Ollama unloaded the model the compose keeps it loaded (OLLAMA_KEEP_ALIVE=-1)
nvidia-smi shows Gemma partly on the CPU not enough GPU memory beside the ears keep Ollama's context at 8192 (the compose does) and nothing else on the card

What it measured#

An RTX 3090 holding the three servers, with the runtime on the same machine; a caller speaking five lines in Spanish, Smart Turn reading the end of each turn. Across a network, add its round trips.

caller what the ears heard caller stops → agent speaks
Hola, buenas. ¿Abren el sábado? Hola, buenas, abren el sábado. 3.78 s (the first turn: Whisper was cold)
Bárbaro. ¿Y cuánto sale un corte? Bárbaro y cuánto sale un corte. 1.51 s
¿Y el color, más o menos cuánto? Y el color más o menos cuanto. 1.57 s
¿Puedo sacar un turno para el sábado a las once? Puedo sacar un turno para el sábado a las 11. 2.11 s
Dale, perfecto. Muchas gracias, chau. Dale perfecto. Muchas gracias. Chao. 1.64 s

Of those ~1.6 s, the ears close the turn about 0.55 s after the caller's last word, Gemma 4's first token takes about 0.7 s, and the voice's first audio 0.1–0.3 s.

What it does not do yet#

  • A tool the model must call. Ollama does not honour tool_choice: "required". The spoken caller of pinecall simulate --voice demands one and fails on Gemma; so does each model judge, which then gives no verdict. An agent's own tools are called when the model chooses to. A server that constrains its output to the call (llama.cpp's llama-server --jinja, or vLLM) fixes it; only the row's base_url changes.
  • Two calls at once share one Ollama, which answers one request at a time per model by default. For more than a demo, serve the model with vLLM, which also speaks the OpenAI API.

Next steps#