Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bdc8b2de2 | ||
|
|
f6fca4705b | ||
|
|
db97159905 | ||
|
|
67760720fc | ||
|
|
c5099e3a8f | ||
|
|
6b609eb4c2 |
@@ -51,25 +51,4 @@ data:
|
|||||||
# even though the DNS CNAME points at the tunnel.
|
# even though the DNS CNAME points at the tunnel.
|
||||||
- hostname: zitadel.riotpiao.com
|
- hostname: zitadel.riotpiao.com
|
||||||
service: https://zitadel.riotpiao.com
|
service: https://zitadel.riotpiao.com
|
||||||
# KUBERNETES API, for kubectl from outside the LAN.
|
|
||||||
#
|
|
||||||
# Origin is the in-cluster Service, not the node IP: that survives a
|
|
||||||
# control-plane address change and lets the connectors reach whichever
|
|
||||||
# apiserver is local-healthy.
|
|
||||||
#
|
|
||||||
# noTLSVerify is required. The apiserver serves a certificate for the
|
|
||||||
# cluster CA plus its node/LB addresses, so its SANs cannot match this
|
|
||||||
# public hostname. Cloudflare terminates the public TLS with the
|
|
||||||
# *.riotpiao.com edge certificate, and the connector hop is already
|
|
||||||
# inside the cluster, so skipping verification on that short hop is the
|
|
||||||
# least-bad option. Do not widen this to other rules.
|
|
||||||
#
|
|
||||||
# SECURITY: this exposes the API to the internet. Authentication is still
|
|
||||||
# ZITADEL OIDC plus client certificates, but put a Cloudflare Access
|
|
||||||
# application in front of this hostname as well -- see the note in the
|
|
||||||
# PR. Without Access, every scanner on the internet reaches the apiserver.
|
|
||||||
- hostname: k8s.riotpiao.com
|
|
||||||
service: https://kubernetes.default.svc.cluster.local:443
|
|
||||||
originRequest:
|
|
||||||
noTLSVerify: true
|
|
||||||
- service: http_status:404
|
- service: http_status:404
|
||||||
|
|||||||
@@ -33,19 +33,17 @@ metadata:
|
|||||||
app: clef-serve
|
app: clef-serve
|
||||||
app.kubernetes.io/part-of: llm-serving
|
app.kubernetes.io/part-of: llm-serving
|
||||||
spec:
|
spec:
|
||||||
# Parked at 0 to free the only V100 that was not serving anything, so candle-vllm can
|
# One card. worker-1 holds four V100s: candle-vllm takes one for the coder, and
|
||||||
# run its Phase 0 on one card. All four cards were allocated (coder 2, reasoning 1,
|
# this pod takes one, so two stay free for a TP=2 candle run.
|
||||||
# this 1) and a new GPU workload had nowhere to land.
|
|
||||||
#
|
#
|
||||||
# This is the only GPU consumer in the namespace that is not vLLM, which is why it is
|
# Enabled 2026-11-04 to serve /v1/systemone, which the gateway already routes to
|
||||||
# the first card released: the standing instruction is to keep the vLLM pods healthy
|
# this Service. It was parked at 0 while candle-vllm was brought up on the same
|
||||||
# until candle-vllm answers, and this pod is not one of them.
|
# cards, and it served no inference request between 2026-10-30T06:23:40Z and then.
|
||||||
#
|
#
|
||||||
# It was serving nothing. The last 30 log lines at park time were 30/30 kubelet
|
# Cold start is about four minutes: the pod pip-installs on every boot because the
|
||||||
# /health probes and no inference request since start (2026-10-30T06:23:40Z). Nothing
|
# torch base image cannot be pushed to the registry (Cloudflare answers 413 above
|
||||||
# in git selects this service. Restore it once the rollout leaves a card free:
|
# 100 MB per layer). startupProbe allows for that.
|
||||||
# 30B-A3B takes 1, Coder-Next TP=2 takes 2, so card 4 is this one.
|
replicas: 1
|
||||||
replicas: 0
|
|
||||||
# Recreate, not RollingUpdate. Every GPU card is allocated, so a rolling update leaves
|
# Recreate, not RollingUpdate. Every GPU card is allocated, so a rolling update leaves
|
||||||
# the new pod Pending forever waiting for a card the old pod still holds.
|
# the new pod Pending forever waiting for a card the old pod still holds.
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Qwen2.5-Coder-14B-Instruct, fp16, tensor-parallel across two V100s.
|
||||||
|
#
|
||||||
|
# Why this model and this precision. The previous workload was Qwen3-Coder-30B-A3B
|
||||||
|
# in GPTQ W4A16 on one V100. Profiling that deployment showed SM utilisation at
|
||||||
|
# 98% while the memory controller sat at 19%: the V100 has FP16 tensor cores and
|
||||||
|
# no INT4 ones, so every decode step ran a dequantisation pass on the SM. That is
|
||||||
|
# a hardware-generation mismatch, not a tunable. Sequence throughput matches the
|
||||||
|
# arithmetic: at batch 1 the step cost equalled a full-model read (17.6 ms against
|
||||||
|
# 15 GB / 900 GB/s = 16.7 ms) while the routed experts alone are ~1.5 GB.
|
||||||
|
#
|
||||||
|
# Measured on this hardware, same harness, same 16-stream load:
|
||||||
|
#
|
||||||
|
# GPTQ W4A16 30B-A3B, TP=1 : ITL 71.5 ms, 215 tok/s aggregate
|
||||||
|
# fp16 14B, TP=2 : ITL 39.7 ms, 287 tok/s aggregate
|
||||||
|
#
|
||||||
|
# The dense model degrades only 1.4x from 1 to 16 streams (27.9 -> 39.7 ms) while
|
||||||
|
# the MoE degrades 4.1x (17.6 -> 71.5 ms), because a dense model reads its weights
|
||||||
|
# once per step and amortises across the batch. The MoE pays expert weight traffic
|
||||||
|
# per sequence. Single-stream latency is the one case where the MoE still wins
|
||||||
|
# (17.6 ms against 27.9 ms), since it activates ~3B parameters per token.
|
||||||
|
#
|
||||||
|
# TP=2 notes that cost time to establish, so they are recorded here:
|
||||||
|
# - Sharding needs no code change. Every dimension divides by 2: attention heads
|
||||||
|
# 40 -> 20, KV heads 8 -> 4, intermediate_size 13824 -> 6912, vocab
|
||||||
|
# 152064 -> 76032. Verify this first for any replacement model.
|
||||||
|
# - TP=2 on a GPTQ checkpoint is what crashed (CUDA_ERROR_ILLEGAL_ADDRESS in
|
||||||
|
# forward()) on the previous model. The cause was quantised weight sharding
|
||||||
|
# and the g_idx guard in src/openai/models/linear.rs, not TP itself. A dense
|
||||||
|
# fp16 checkpoint has no g_idx, no packed weights and no scales, so it takes
|
||||||
|
# the plain column/row-parallel path and the crash does not reproduce.
|
||||||
|
# - These are V100-PCIE cards with no NVLink, so every layer's all-reduce
|
||||||
|
# crosses PCIe. It shows up in TTFT (275 ms at 1 stream, 1090 ms at 16)
|
||||||
|
# more than in ITL.
|
||||||
|
# - KV capacity is 11635 MB per rank, about 59K context tokens. The fp16
|
||||||
|
# weights consume the room the KV pool would otherwise use. This is well
|
||||||
|
# short of the 256K the previous model targeted, and it is the price of
|
||||||
|
# leaving the dequantisation path.
|
||||||
|
#
|
||||||
|
# Weights must be in the PVC as /mnt/models/coder-14b-fp16 (28 GB, 6 shards).
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: candle-vllm
|
||||||
|
namespace: llm-serving
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
app: candle-vllm
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
# One replica holds both GPUs, so a rolling update would need a second pair.
|
||||||
|
# Recreate keeps the rollout to a single weights load.
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: candle-vllm
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
app: candle-vllm
|
||||||
|
spec:
|
||||||
|
# Required. Without this the container runs under runc, libcuda.so is never
|
||||||
|
# injected, and candle-vllm panics in cudarc at startup with
|
||||||
|
# "Unable to dynamically load the cuda shared library".
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
containers:
|
||||||
|
- name: candle
|
||||||
|
image: forgejo.riotpiao.com/riotpiao-poimen/candle-vllm:sm70-gptq-slim
|
||||||
|
command: ["/usr/local/bin/candle-vllm"]
|
||||||
|
# fp16, not GPTQ, and two ranks, not one. See the header for the
|
||||||
|
# measurements and the TP=2 findings.
|
||||||
|
args:
|
||||||
|
- --w
|
||||||
|
- /mnt/models/coder-14b-fp16
|
||||||
|
- --d
|
||||||
|
- 0,1
|
||||||
|
- --dtype
|
||||||
|
- f16
|
||||||
|
- --p
|
||||||
|
- "8080"
|
||||||
|
- --max-num-seqs
|
||||||
|
- "32"
|
||||||
|
# 11.6 GB of KV per rank, about 59K context tokens. The fp16 weights
|
||||||
|
# leave no room for more.
|
||||||
|
- --kv-fraction
|
||||||
|
- "0.85"
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: "8"
|
||||||
|
memory: 36Gi
|
||||||
|
nvidia.com/gpu: "2"
|
||||||
|
requests:
|
||||||
|
cpu: "4"
|
||||||
|
memory: 12Gi
|
||||||
|
nvidia.com/gpu: "2"
|
||||||
|
volumeMounts:
|
||||||
|
- name: models
|
||||||
|
mountPath: /mnt/models
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Stable client-facing name for the coder chat model, deliberately decoupled
|
||||||
|
# from the process that serves it.
|
||||||
|
#
|
||||||
|
# poimen-memory addresses a Service, not a pod and not an InferenceService. The
|
||||||
|
# vLLM -> candle-vllm handoff therefore changes this file's selector and nothing
|
||||||
|
# else, so the client config never changes again and there is no window in which
|
||||||
|
# the host name is wrong.
|
||||||
|
#
|
||||||
|
# The previous KTML predictor requested two V100s and never reached Ready, so it
|
||||||
|
# held half the GPU pool while serving nothing. candle-vllm replaces it.
|
||||||
|
#
|
||||||
|
# Two ports on purpose:
|
||||||
|
# - 80 matches every other predictor Service in this namespace, and matches
|
||||||
|
# a client base URL that carries no port number.
|
||||||
|
# - 8080 is the container port. Publishing it directly avoids a port
|
||||||
|
# translation that is easy to get wrong in a curl.
|
||||||
|
#
|
||||||
|
# Do not delete and recreate this Service: a delete drops the ClusterIP, and a
|
||||||
|
# client that cached the name during the gap fails until it re-resolves.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: coder-predictor
|
||||||
|
namespace: llm-serving
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
app: coder-predictor
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: candle-vllm
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
- name: http-8080
|
||||||
|
protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
@@ -13,14 +13,19 @@ resources:
|
|||||||
# clef-flash runs through clef-serve.yaml, not through an SGLang runtime. SGLang needs
|
# clef-flash runs through clef-serve.yaml, not through an SGLang runtime. SGLang needs
|
||||||
# v0.5.22+, published only on CUDA 13, and CUDA 13 dropped Volta kernels. The model
|
# v0.5.22+, published only on CUDA 13, and CUDA 13 dropped Volta kernels. The model
|
||||||
# instead runs the implementation it ships (joint_schema_model.py) in fp16 on one V100.
|
# instead runs the implementation it ships (joint_schema_model.py) in fp16 on one V100.
|
||||||
|
# coder-14b-fp16.yaml owns the `candle-vllm` Deployment name and the
|
||||||
|
# `app: candle-vllm` label that coder-service.yaml selects. It must be the only
|
||||||
|
# workload with that label, or the Service splits traffic across two models.
|
||||||
|
#
|
||||||
|
# fp16 on two V100s, not GPTQ on one: measured 39.7 ms ITL and 287 tok/s at 16
|
||||||
|
# streams against 71.5 ms and 215 tok/s for the 4-bit MoE. The V100 has FP16
|
||||||
|
# tensor cores and no INT4 ones, so 4-bit pays dequantisation on the SM while
|
||||||
|
# the memory controller idles at 19%.
|
||||||
|
- coder-14b-fp16.yaml
|
||||||
- clef-serve.yaml
|
- clef-serve.yaml
|
||||||
# coder = Qwen3-Coder-30B-A3B-Instruct, 4-bit GPTQ, TP=2 at 262,144 tokens. It serves
|
|
||||||
# both `coder` and `reasoning`, so poimen-memory needs a host change only, never a
|
|
||||||
# model-name change. reasoning.yaml below can be dropped once that host change lands.
|
|
||||||
- coder.yaml
|
|
||||||
- reasoning.yaml
|
|
||||||
# Stable client-facing name for the chat model. poimen-memory points at this Service,
|
# Stable client-facing name for the chat model. poimen-memory points at this Service,
|
||||||
# so the vLLM -> candle-vllm handoff is a selector edit here and no client change.
|
# so the vLLM -> candle-vllm handoff is a selector edit here and no client change.
|
||||||
|
- coder-service.yaml
|
||||||
- reasoning-long-service.yaml
|
- reasoning-long-service.yaml
|
||||||
# ornith is retired: its InferenceService still reports Ready=True while running zero
|
# ornith is retired: its InferenceService still reports Ready=True while running zero
|
||||||
# replicas, because desired equals available at 0. Its manifest was never committed,
|
# replicas, because desired equals available at 0. Its manifest was never committed,
|
||||||
|
|||||||
Reference in New Issue
Block a user