llm-d-sc semantic-routing quickstart¶
This quickstart runs the llm-d-sc semantic-routing demo
with explicit Podman networking. It does not use OpenShift, RHOAI, KServe, Routes, or podman compose.
Deterministic tests use synthetic OpenAI-compatible backends. GitHub CI checks image pins, container hardening, classifier readiness, routing and failover, WebUI, and trace API without Qwen weights or a GPU. A manual AWS scenario validates real vLLM.
Architecture¶
The optional browser path adds Open WebUI and a repository-owned trace relay:
Open WebUI
-> trace-ui :8080
-> Praxis :8080
-> llm-d-sc classifier :50051
-> trace-ui backend relay :8081 or :8082
-> synthetic vllm-cpu or vllm-gpu backend
The front relay records requests in SQLite and forwards them to Praxis. Backend
relays add X-Demo-Route and X-Demo-Classification and record attempts,
including failovers. The dashboard is at /; traces are at /_trace/api/traces.
The validated AWS topology splits CPU and GPU work across two hosts:
CPU host (m7i.4xlarge)
Open WebUI + trace-ui + Praxis + llm-d-sc + vllm-cpu
|
| private VPC TCP/8000
v
GPU host (g6.8xlarge)
vllm-gpu only
This split is required because the selected G6 shape lacks the AVX-512 or AMX
instructions required by the pinned CPU vLLM image. Both hosts serve
RedHatAI/Qwen3.8-27B-INT4; llm-d-sc routes by prompt complexity.
Run the deterministic test¶
From an amd64 Linux host with Podman running:
The integration test pulls pinned images, creates a temporary network and
model volume, waits for classifier readiness, sends simple and reasoning
prompts through WebUI, checks traces, then cleans up. Pass --keep to leave the
stack running for OpenCode. Synthetic
backends also return OpenAI-compatible events for streaming clients.
A successful run reports:
SIMPLEwithx-llm-d-sc-status: OK, routed tovllm-cpu.REASONINGwithx-llm-d-sc-status: OK, routed tovllm-gpu.- A synthetic CPU 503 fails over to the alternate backend and records both hops.
- Open WebUI health, model discovery, chat, and trace API checks passed.
GitHub CI runs this path with Docker; it does not test real generation, GPU scheduling, model quality, throughput, or latency.
Run the AWS split-host scenario¶
This manual, billable scenario uses two amd64 RHEL 9 hosts in one VPC with
rootless Podman, 200 GiB disk on CPU and 300 GiB on GPU. CPU needs AVX-512 or
AMX; the validated g6.8xlarge GPU host had an NVIDIA L4, driver, and CDI
device (nvidia.com/gpu=0).
The sizing presets are:
configs/aws/llm-d-sc-webui-cpu.json:m7i.4xlarge, 64 GiB RAM.configs/aws/llm-d-sc-webui.json:g6.8xlarge, one L4 GPU, 128 GiB RAM.
Allow only the CPU security group to reach GPU TCP/8000; do not expose it publicly. Enable user lingering on both hosts before starting containers over SSH:
On the GPU host, start only the backend on its private address; replace
GPU_PRIVATE_IP with that host's private IPv4 address:
CONTAINER_ENGINE=podman \
LLM_D_SC_BACKEND_TIMEOUT=720 \
python3 tests/llm-d-sc/integration.py \
--real --gpu-only --gpu-publish-host GPU_PRIVATE_IP --keep
On the CPU host, run the complete stack against that endpoint:
CONTAINER_ENGINE=podman \
LLM_D_SC_BACKEND_TIMEOUT=720 \
python3 tests/llm-d-sc/integration.py \
--real --remote-gpu GPU_PRIVATE_IP --keep
Tunnel to the loopback WebUI URL printed by the test, replacing the port:
Open http://127.0.0.1:8080; reach the trace dashboard using its printed port.
Add --cache-volume NAME to reuse a named Podman cache and avoid downloading
the 18 GiB model again. The test does not remove named caches.
Run OpenCode through the routed backend¶
Run this with an integration stack still up (--keep), either mocked or
split-host. OpenShell must run on the stack host so its sandbox can reach
host.openshell.internal. Set PRINTED_TRACE_PORT to the port in the printed
trace_ui.url, then create an OpenCode workload using this relay as its model gateway:
os_run env PRAXIS_PORT=PRINTED_TRACE_PORT \
openshell/harnesses/opencode/create.sh \
--profile dev \
--name opencode-llm-d-sc \
--config "$PWD/configs/llm-d-sc"
The template supplies model ID Semantic-Qwen. The strict policy permits only
model discovery and chat completion through the trace relay to Praxis. The
provider marks the model as reasoning but does not replay interleaved reasoning,
which can confuse Qwen's parser after tool calls.
AWS validation record¶
The split-host scenario was validated on 2026-10-20 in us-east-1:
| Check | Result |
|---|---|
| CPU host | m7i.4xlarge, AVX-512 available, 64 GiB RAM |
| GPU host | g6.8xlarge, NVIDIA L4, driver 580.178.04 |
| Model | RedHatAI/Qwen3.8-27B-INT4, 18.12 GiB weights |
| Simple prompt | SIMPLE -> vllm-cpu, HTTP 200, 19.342 s |
| Reasoning prompt | REASONING -> vllm-gpu, HTTP 200, 10.086 s |
| Open WebUI | health, model list, and chat passed |
| Trace API | CPU and GPU routes both recorded |
These are single-request measurements, not a benchmark. GPU load used 16.84 GiB VRAM and steady serving about 20.5 GiB. Real-mode prompts disable Qwen thinking so validation receives final content within the token budget.
OpenCode was also validated on disposable AWS m7i.2xlarge RHEL 9.6 with
OpenShell v0.1.4 and mocked backends; simple and reasoning prompts each routed
to the expected CPU or GPU backend in one turn.
Security boundary¶
Host-published listeners stay on loopback. Praxis and backends use a private container network; split-host GPU traffic uses private VPC addressing and a source security-group rule.
insecure_options.allow_private_upstreams: true disables upstream SSRF
protection for private backends. Do not expose the listener publicly without a
reviewed network and authorization boundary.
WEBUI_AUTH=false is only for a disposable loopback demo. Shared deployments
need authentication and HTTPS. The trace relay stores prompts and responses
(up to 16,000 characters) in temporary SQLite; treat this data as sensitive.
Temporary image¶
The integration uses the filter-enabled Praxis image pinned in
images.env:
quay.io/rmalone/praxis-ai-llm-d-sc@sha256:1db1eb2a115273836a186018f20df32197336a14ed2026cede8e0946385893dc
This linux/amd64 image's registry metadata identifies
praxis-proxy/ai as its source but does
not include a source revision. It is the temporary digest-pinned input approved
for this integration in issue #73;
issue #74 tracks
consolidating llm_d_sc into the reviewed Praxis image. Keep the digest pin;
do not use the mutable latest tag.