Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deploy/systemd/genie-api.service
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Restart=always
RestartSec=5
MemoryMax=32M

# Pin to the system bucket (issue #25) so the dashboard's HTTP loop doesn't
# steal cycles from whisper (2-3), llama-server (4), or genie-core (5).
CPUAffinity=0 1

ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/geniepod/data
Expand Down
9 changes: 9 additions & 0 deletions deploy/systemd/genie-core.service
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ StandardInput=null
Restart=always
RestartSec=3

# CPU pinning for voice latency stability (issue #25). Core 5 hosts the
# orchestrator and every audio child it spawns (piper, sox, deep-filter,
# arecord, aplay) — they inherit this affinity. Keeping the TTS pipeline on
# one core shields whisper (2-3) and llama-server (4) from preemption during
# decode. arecord runs on core 5 by inheritance (not 0-1 as the issue's
# system-bucket bullet suggests); revisit with per-spawn taskset only if
# capture jitter shows up under load.
CPUAffinity=5

# Needs GPU (Whisper TRT, Piper TTS), audio (ALSA/I2S), and network.
ProtectSystem=no
SupplementaryGroups=video render audio
Expand Down
4 changes: 4 additions & 0 deletions deploy/systemd/genie-governor.service
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ WatchdogSec=30
MemoryMax=32M
CPUWeight=50

# Pin to the system bucket (issue #25) — governor only polls memory and writes
# drop-in configs, keep it off the inference cores (2-5).
CPUAffinity=0 1

# Security hardening
ProtectSystem=strict
ProtectHome=yes
Expand Down
4 changes: 4 additions & 0 deletions deploy/systemd/genie-health.service
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ WatchdogSec=60
MemoryMax=32M
CPUWeight=20

# Pin to the system bucket (issue #25) — health monitor is a periodic poller,
# keep it off the inference cores (2-5).
CPUAffinity=0 1

# Security hardening
ProtectSystem=strict
ProtectHome=yes
Expand Down
8 changes: 8 additions & 0 deletions deploy/systemd/genie-llm.service
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ Restart=on-failure
RestartSec=5
TimeoutStartSec=120

# CPU pinning for voice latency stability (issue #25). Inference is GPU-bound
# (--n-gpu-layers 999), so one dedicated core is enough to host CUDA dispatch
# + sampler + tokenizer without contending with whisper (2-3) or genie-core
# (5). --threads=4 above stays as-is for now; with a single-core affinity the
# four worker threads time-share core 4 — fine for the GPU-bound critical path
# but worth revisiting (--threads 1 or 2) if prompt-eval shows scheduler churn.
CPUAffinity=4

# GPU needs full system access
ProtectSystem=no
SupplementaryGroups=video render
Expand Down
4 changes: 4 additions & 0 deletions deploy/systemd/genie-mqtt.service
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Restart=always
RestartSec=3
MemoryMax=16M

# Pin to the system bucket (issue #25) — broker traffic is light and event-
# driven, keep it off the inference cores (2-5).
CPUAffinity=0 1

ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/geniepod/data/mqtt
Expand Down
8 changes: 6 additions & 2 deletions deploy/systemd/genie-wakeword.service
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ ExecStart=/usr/bin/env python3 /opt/geniepod/bin/genie-wakeword.py
Restart=always
RestartSec=1

# Pin to isolated CPU cores for real-time audio processing.
CPUAffinity=4 5
# Pin to audio/system cores for real-time wake-word detection (issue #25).
# Cores 4 and 5 are now reserved for llama-server and genie-core respectively;
# the wake-word listener belongs alongside ALSA + capture on the system bucket.
# SCHED_FIFO at priority 50 is kept so the continuous audio loop isn't
# preempted by best-effort system tasks sharing these cores.
CPUAffinity=0 1
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=50

Expand Down
6 changes: 6 additions & 0 deletions deploy/systemd/genie-whisper.service
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ Restart=on-failure
RestartSec=5
TimeoutStartSec=120

# CPU pinning for voice latency stability (issue #25). Dedicate two cores to
# whisper-server so STT decode does not contend with llama-server (core 4) or
# the audio capture / TTS pipeline (cores 0-1 and 5). --threads=2 matches the
# affinity so each decode thread gets its own core.
CPUAffinity=2 3

# GPU needs full system access (same posture as genie-llm).
ProtectSystem=no
SupplementaryGroups=video render
Expand Down
42 changes: 42 additions & 0 deletions doc/deployment-and-ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,48 @@ Operator decisions before enabling stricter policy:
- Use `[core.tool_policy]` allowlists/denylists per channel when a surface should be less capable than local dashboard/API.
- Keep `unknown` out of physical actuation origins unless there is a controlled reason to allow it.

## CPU Pinning (Voice Latency Stability)

The Jetson Orin Nano has six CPU cores. The voice path (wake → STT → LLM → TTS
→ playback) is sensitive to scheduler jitter when multiple inference servers
and audio subprocesses share cores. Each `genie-*` systemd unit ships with a
`CPUAffinity=` directive that partitions the six cores into four buckets
(issue #25):

| Cores | Workload |
| --- | --- |
| 0–1 | Kernel, ALSA, MQTT broker, `genie-api`, `genie-governor`, `genie-health`, `genie-wakeword` |
| 2–3 | `whisper-server` (STT decode, two threads) |
| 4 | `llama-server` (GPU-bound; one core hosts CUDA dispatch + sampler) |
| 5 | `genie-core` and all audio children it spawns (`piper`, `sox`, `deep-filter`, `arecord`, `aplay`) |

`genie-wakeword` retains `SCHED_FIFO` at priority 50 so the continuous audio
loop is not preempted by best-effort work sharing cores 0–1.

Verify pinning after a deploy / restart:

```bash
# Per-service: confirm the unit and its children are on the expected cores.
for svc in genie-core genie-llm genie-whisper genie-wakeword genie-api \
genie-governor genie-health genie-mqtt; do
pid=$(systemctl show -p MainPID --value "${svc}.service")
[ "$pid" != "0" ] && printf "%-18s PID=%s affinity=%s\n" \
"$svc" "$pid" "$(taskset -pc "$pid" | awk -F': ' '{print $2}')"
done

# All threads of one service (useful for whisper / llama with multi-threading):
ps -L -o pid,tid,psr,comm -p "$(pidof whisper-server)"

# Live core distribution while a voice cycle runs (Jetson-specific):
sudo tegrastats --interval 250
```

Acceptance signal (issue #25): ten consecutive voice cycles should hold STT
latency within ±100 ms of the median once warmup has completed. If variance
persists after Option 1, the next step is kernel-level `isolcpus=2,3,4,5` on
the bootloader command line — that is intentionally out of scope here because
it requires a Jetson reflash / extlinux.conf edit.

## Runtime Data And State

Default production data location:
Expand Down