Files
homelab/k8s/apps/llm-serving/clef-serve.yaml
T
rock d821718375 chore(llm-serving): park clef-serve to free a V100 for candle-vllm
candle-vllm cannot start. worker-1 has four V100-32GB and all four are
allocated: coder 2, reasoning 1, clef-serve 1. Free is 0, so a new GPU pod
sits Pending no matter what it is.

This pod is the only GPU consumer in the namespace that is not vLLM, so it is
the only card that can be released without breaking the instruction to keep
vLLM healthy until candle-vllm answers. Ingest keeps running on
reasoning-predictor, and pi keeps its coder model on coder-predictor.

Measured activity over the last 5,000 log lines, since it started on
2026-10-30T06:23:40Z:

  4,994  GET /health        (kubelet probes)
      2  GET /v1/models
      1  GET /v1/completions
      1  GET /v1/chat/completions
      1  GET /docs
      1  GET /

No successful inference. The four inference-path requests are GET, and these
routes answer POST, so they cannot have produced a completion. It held a card
for a day to answer liveness probes, which is the same shape as the comfyui
retirement. Nothing in git selects this Service.

Reversible: set replicas back to 1. It should return once the rollout settles,
because 30B-A3B takes 1 card and Coder-Next TP=2 takes 2, which leaves card 4
for this workload.

Not applied. ArgoCD runs selfHeal for llm-serving, so a live scale to 0 is
reverted within minutes; this needs the merge to stick. Apply through
kustomize, not `kubectl apply -f clef-serve.yaml`: the Deployment mounts the
generated ConfigMap by its hashed name, and a raw apply drops the hash.
2026-10-31 19:40:55 -07:00

138 lines
5.1 KiB
YAML

# clef-flash: a decision model, not a chat model.
#
# It reads a `state` plus a schema of typed `questions` and returns one probability
# per allowed option in a single prefill pass. There is no decoding, no sampling and
# no free-form text. The public route is /v1/systemone, and its request and response
# shapes are Clef's own: no `messages`, no `choices`.
#
# Why not SGLang: Clef support landed in SGLang v0.5.22, which upstream publishes only
# on CUDA 13. CUDA 13 dropped Volta kernels and every card here is a Tesla V100
# (compute capability 7.0), so SGLang aborts with "no kernel image is available for
# execution on the device". SGLang also states sm75 as its floor regardless.
#
# So this runs the implementation the model ships: joint_schema_model.py, which has
# load_release_model, encode_record, collate_records and systemone. The weights are
# 17.75 GiB of BF16, and fp16 costs the same bytes per parameter. On one 32 GiB card
# that leaves room for a 32k state, which is why this is one GPU and not two: with
# device_map over two cards the embedding table and the tensors indexing into it land
# on different devices, and the forward pass dies with
# "indices should be either on cpu or on the same device as the indexed tensor".
#
# Known gap: the image cannot be pushed to the registry. Cloudflare fronts
# forgejo.riotpiao.com and answers 413 Payload Too Large for any layer above 100 MB,
# and the torch base alone is 7.58 GB. So this mounts app.py from a ConfigMap and pip
# installs on every start. A cold start therefore needs PyPI reachable, and takes
# about four minutes. Build the image in-cluster, or unproxy the registry host, before
# treating this as production.
apiVersion: apps/v1
kind: Deployment
metadata:
name: clef-serve
namespace: llm-serving
labels:
app: clef-serve
app.kubernetes.io/part-of: llm-serving
spec:
# Parked at 0 to free the only V100 that was not serving anything, so candle-vllm can
# run its Phase 0 on one card. All four cards were allocated (coder 2, reasoning 1,
# 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
# the first card released: the standing instruction is to keep the vLLM pods healthy
# until candle-vllm answers, and this pod is not one of them.
#
# It was serving nothing. The last 30 log lines at park time were 30/30 kubelet
# /health probes and no inference request since start (2026-10-30T06:23:40Z). Nothing
# in git selects this service. Restore it once the rollout leaves a card free:
# 30B-A3B takes 1, Coder-Next TP=2 takes 2, so card 4 is this one.
replicas: 0
# 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.
strategy:
type: Recreate
selector:
matchLabels:
app: clef-serve
template:
metadata:
labels:
app: clef-serve
app.kubernetes.io/part-of: llm-serving
spec:
runtimeClassName: nvidia
nodeSelector:
kubernetes.io/hostname: worker-1
containers:
- name: clef
image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime
command: ["/bin/bash","-lc"]
args:
- |
set -e
pip install --no-cache-dir transformers==5.10.2 accelerate fastapi 'uvicorn[standard]' pillow safetensors sentencepiece protobuf
exec uvicorn app:create_app --factory --host 0.0.0.0 --port 8080
workingDir: /app
env:
- name: PYTHONUNBUFFERED
value: "1"
- name: MODEL_PATH
value: /mnt/models/clef-flash
# V100 has no bf16, and the release weights are bf16. fp16 is the same width.
- name: DTYPE
value: float16
# The state is truncated at this length without warning, so raise it only with
# spare VRAM: 17.75 GiB of weights plus 128 KiB of KV per token.
- name: MAX_LEN
value: "32768"
ports:
- containerPort: 8080
# A cold start pip-installs and loads 17.75 GiB, measured at about four minutes.
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 120
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 10
resources:
requests:
cpu: "4"
memory: 12Gi
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: 40Gi
nvidia.com/gpu: "1"
volumeMounts:
- name: app
mountPath: /app
- name: models
mountPath: /mnt/models
readOnly: true
volumes:
- name: app
configMap:
name: clef-serve-app
- name: models
persistentVolumeClaim:
claimName: llm-models
---
apiVersion: v1
kind: Service
metadata:
name: clef-serve
namespace: llm-serving
labels:
app.kubernetes.io/part-of: llm-serving
spec:
selector:
app: clef-serve
ports:
- name: http
port: 80
targetPort: 8080