Files
helexa/models.example.toml
T
grenade 821ca116aa feat(sampling): let the operator correct a model's published temperature
A model's `generation_config.json` is its authors' statement of intent,
and honouring it (#272) is right by default. It is not always right for
the workload being served: `Qwen/Qwen3.8-27B` publishes
`temperature = 1.0`, which measured a 20% structural-defect rate on
~2k-token code generation against 0/60 at `<= 0.6` — Fisher exact
one-sided p = 0.0031, n = 20 per arm, truncation excluded by
`finish_reason` (#283).

Extrapolated at constant hazard that is a ~79% chance of at least one
defect in a 15k-token file, which is why the model had never once
finished a task two cloud models did in ten minutes. The failures are
structural, not semantic — an `else` with no `if`, a duplicate `export
function` — the sampling-tail signature, not a capability gap.

Adds `SamplingOverride { temperature, top_p, top_k }` to `ModelSpec`
(which doubles as the `[[default_models]]` shape in neuron.toml) and to
`ModelProfile` (models.toml), so the override travels with the *model*
rather than the request. neuron is the only place sampling is resolved,
so it has to reach there whether the load came from cortex's catalogue
or the host's own config.

Precedence is request > operator > model > fallback. The operator sits
below the request deliberately: an explicit API parameter that is
silently ignored is a contract break, and the observed failure came from
callers naming nothing, so a default-tier override is sufficient.

Mechanically the operator tier is folded into the model tier once at
load by `with_operator_override`, so the per-request path stays a
two-tier lookup and none of the five resolve sites can forget it — the
same reasoning that made #272 resolve sampling once instead of at eight
`LogitsProcessor` construction sites. Fields overlay independently, so
correcting `temperature` does not blank `top_p`/`top_k` back to "no
truncation" (the #271 failure mode).

Deliberately not a heuristic. neuron will never inspect a model and
guess a better temperature; a guesser is a footgun, a number in a config
file is not.

Two files now describe one model, which is exactly the #252 hazard, so
the gate lands with the feature rather than after the next incident:

- `script/check-config-consistency.py` fails when models.toml and
  asset/neuron/<host>.toml disagree on `quant` or `sampling` for a model
  they both name. It catches #252 retroactively.
- CI gains a `config` job, ungated by the change classifier — a
  TOML-only push is neither `rust` nor `web`, and that is precisely the
  push that introduces drift.
- deploy.yml gains a `config-check` gate ahead of both rollouts, and now
  actually *ships* the configs: models.toml to the gateway and
  asset/neuron/<host>.toml to each neuron, digest-compared, with a
  change restarting the service even when no new RPM landed. The sudoers
  grants for both rsync targets already existed and were simply unused.

models.toml is untracked no longer. It carries no secrets — cortex.toml
holds the API keys — and CI cannot gate a file it cannot see, which is
how one half of the #252 pair stayed invisible.

Refs #283, #272, #271, #252
2026-08-25 12:47:56 +03:00

210 lines
10 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# models.example.toml — model catalogue
#
# Copy to /etc/cortex/models.toml and adjust for your environment.
# Describes how to serve each model. Cortex matches these profiles
# against discovered neuron topologies for placement decisions; the
# resulting `(catalogue × topology)` set is what `GET /v1/models`
# returns and what the router can cold-load on demand.
#
# Field reference:
# id - Repo id in the source registry (e.g. "Qwen/Qwen3.6-27B").
# Exact match.
# harness - which engine handles inference (currently "candle").
# quant - quantisation to serve this model with. Either a
# GGUF tag naming a file in the repo (e.g.
# "Q4_K_M"), or an in-situ tag applied to dense
# safetensors at load (e.g. "q6k", "q4k", "q8_0")
# — the latter is what TP models use, since TP
# requires dense weights.
#
# SET THIS whenever the model is served
# quantised, even if a neuron's `default_models`
# already says so. The two are separate paths to
# the same model: activation reads neuron.toml,
# cold-load reads this file. Omitting it here
# means a cold-load silently loads full-precision
# weights, which for a large model can leave too
# little VRAM to prefill — it reports `loaded`
# and then fails every request (#252).
#
# Omit only when full precision is genuinely what
# you want served.
# sampling - operator sampling override (#283). Optional
# sub-table `[models.sampling]` with any of
# `temperature`, `top_p`, `top_k`.
#
# Precedence is request > operator > model's
# generation_config.json > built-in fallback: this
# replaces what the model published, but a caller
# that names a value still wins.
#
# Use when a model's own published sampling is
# wrong for the workload you serve. Qwen3.8-27B
# publishes temperature = 1.0, which measured a
# 20% structural-defect rate on ~2k-token code
# generation against 0/60 at <= 0.6 (Fisher exact
# one-sided p = 0.0031, #283).
#
# Like `quant`, this must MATCH the same model's
# `[default_models.sampling]` in the host's
# neuron.toml — the same two-paths-to-one-model
# hazard that made #252 possible.
# `script/check-config-consistency.py` gates it.
# vram_mb - rough estimate; advisory only, not enforced.
# min_devices - GPU count this profile needs. TP profiles use
# the same value as the tensor-parallel size.
# min_device_vram_mb - each device must meet this VRAM floor for the
# neuron to be considered "feasible".
# pinned_on - optional whitelist of neuron names. Non-empty
# narrows feasibility to just those neurons.
# Affinity only: it says where the model may run,
# not whether it may be evicted once there.
# residency_priority - which residency CLASS this model belongs to
# when a node runs out of VRAM. A model may
# displace residents of its own class or below,
# and none above it. Default 100.
#
# Equal rank permits displacement, in BOTH
# directions — that is what a cold-swap is. Two
# models that share a node and take turns on it
# must each be able to evict the other, so give
# peers the SAME number. Giving them different
# numbers makes the swap one-directional: the
# higher takes the node and the lower never gets
# it back, silently.
#
# Protect a model by ranking it ABOVE whatever
# must not evict it — not by giving it a number
# nobody else has.
#
# This is what lets a fleet say "the image model
# may take the mid tier's node, but never the
# flagship's, while the frontier tier may take
# the flagship's" — three rules that a single
# pinned/unpinned flag cannot express, because it
# can only protect a model from everything or
# from nothing.
#
# Priority decides who MAY be displaced, never
# whether a displacement is NEEDED: a node with
# room for both evicts nothing, however the two
# rank.
#
# A profile with pinned_on and no explicit
# priority defaults to 1000, preserving the
# immunity pinned_on used to imply on its own.
#
# See https://helexa.ai/docs/operating/placement for
# worked examples.
# source - optional source scheme ("huggingface", "helexa",
# operator mirror tag). When set, cortex forwards
# the load to neuron as `scheme:id` so the daemon
# fetches from the right registry. Omit to let
# neuron substitute its own `default_source`.
# cost.* - optional operator-set pricing, surfaced verbatim on
# GET /v1/models for clients (opencode) to display
# spend. USD per 1,000,000 tokens, as numbers:
# cost.input prompt tokens
# cost.output completion tokens
# cost.cache_read cache-hit tokens (optional tier)
# cost.cache_write cache-write tokens (optional tier)
# Absent vs zero is intentional (#68): OMIT the whole
# cost block to mean "price not declared / unknown";
# set cost.input/output = 0.0 to mean "intentionally
# free" (self-hosted). The advertised rate must match
# what metering bills against.
# Tensor-parallel target — needs a neuron with at least 2 large GPUs.
# The example pins to a specific neuron name; adjust or remove the
# pinned_on entry for your own fleet.
[[models]]
id = "Qwen/Qwen3.6-27B"
harness = "candle"
vram_mb = 54000
min_devices = 2
min_device_vram_mb = 24000
pinned_on = ["your-multi-gpu-neuron"]
# Above the everyday models, so none of them can cold-swap the flagship
# out. Give anything that SHOULD be able to take this neuron — a second
# large model you alternate with — the same 300, not a higher number.
residency_priority = 300
# Token budget: context wall, compaction trigger (input headroom), max output.
limit.context = 32768
limit.input = 28672
limit.output = 4096
# Pricing in USD per 1M tokens. Explicit 0.0 = intentionally free
# (self-hosted) — distinct from omitting `cost`, which means "not priced".
cost.input = 0.0
cost.output = 0.0
# Optional, and usually unnecessary (#241). Neurons derive a model's
# modalities from their local cache — `model_index.json` at a repo root
# means image generation, a `vision_config` in `config.json` means
# vision — so `/v1/models` reports them whether or not anything is
# loaded. Discovery wins where the two disagree, because it is read from
# the weights that would actually be served; this field is a fallback
# for a model no node has downloaded yet.
capabilities = ["text", "reasoning"]
# Mid-size dense model — fits on any single GPU with ≥16 GB VRAM.
# No `cost` block here: this model is "not priced" — /v1/models omits the
# `cost` key for it, so opencode shows spend as unknown rather than $0.
[[models]]
id = "Qwen/Qwen3-8B"
harness = "candle"
vram_mb = 18000
min_devices = 1
min_device_vram_mb = 16000
limit.context = 16384
limit.output = 4096
# Small GGUF quantised — runs on any small GPU.
[[models]]
id = "unsloth/Qwen3-0.6B-GGUF"
harness = "candle"
quant = "Q4_K_M"
vram_mb = 500
min_devices = 1
min_device_vram_mb = 4000
limit.context = 8192
limit.output = 2048
# Helexa registry model — `source` pins this entry to the helexa
# scheme so cortex forwards `helexa:Helexa/Qwen3.6-27B-Uncensored` to
# neuron's /models/load. Requires the neuron config to declare a
# matching [harness.candle.sources.helexa] entry pointing at the
# helexa registry endpoint (see neuron.example.toml).
#
# [[models]]
# id = "Helexa/Qwen3.6-27B-Uncensored"
# harness = "candle"
# source = "helexa"
# vram_mb = 54000
# min_devices = 2
# min_device_vram_mb = 24000
# -- Tier aliases ------------------------------------------------------------
# Optional. Clients can request inference against an alias (e.g.
# `model: "helexa/small"` in /v1/chat/completions) and cortex
# transparently routes to the concrete model id below — including
# rewriting the body's model field so neuron sees a name that matches
# its loaded handle. Both the alias and the target appear in
# /v1/models so clients can discover either. Operators can swap
# targets here without changing client code.
#
# [aliases]
# "helexa/small" = "Qwen/Qwen3-1.7B"
# "helexa/balanced" = "Qwen/Qwen3-8B"
# "helexa/large" = "Qwen/Qwen3.6-27B"
# Text-to-image (#197): Z-Image-Turbo (Apache 2.0). 6B S3-DiT + Qwen3-4B
# text encoder (host CPU by default; see [harness.candle.image]) + VAE.
# BF16 DiT+VAE resident ~= 14.6 GB -> any 20 GB+ card. Cold-loads on the
# first /v1/images/generations request; metered in megapixel-steps, not
# tokens.
[[models]]
id = "Tongyi-MAI/Z-Image-Turbo"
harness = "candle"
vram_mb = 15000
min_devices = 1
min_device_vram_mb = 20000