mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Une tête MTP (Qwen3.6/3.8, intégrée ou publiée à part en mtp-*.gguf) accélère
le décodage sans rien changer à la sortie : chaque jeton émis est tiré par
l'échantillonneur du modèle cible, un jeton du brouillon n'est gardé que s'il
coïncide. Le prix est ailleurs — 1 à 2 Go de VRAM, un prefill parfois plus
lent, une fonction très récente du moteur — d'où l'opt-in : rien ne change
pour un preset existant.
- SPEC=off (défaut, clé absente comprise) / auto / mtp. La tête se reconnaît
au TENSEUR blk.{N-1}.nextn.eh_proj (backend_gguf.go), comme llama.cpp : la
clé nextn_predict_layers seule ne prouve rien.
- MODEL_DRAFT : résolu comme MMPROJ, mais introuvable ne bloque pas le
lancement. Tête à part → -md + --spec-type draft-mtp explicite (llama.cpp ne
devine que sur la première tranche) ; autre brouillon → draft-simple, sinon
chargé en VRAM pour rien.
- auto seulement si --fit place tout (ni couches chiffrées, -ts, -ot, experts
sur CPU, -sm row, --fit off, -dev), contexte chiffré (fit pourrait sinon le
réduire), pas de vision, build officiel ≥ 11009, jamais qwen4exp. mtp impose,
avec un avertissement.
- Aide du moteur lue à chaque fois : draft-mtp et --spec-draft-n-max requis ;
--draft/--draft-max jamais émis. EXTRA_ARGS (--spec-type, -md, -hfd,
--spec-default…) et LLAMA_ARG_SPEC_* gardent la main.
- Tirage greedy fixé (exact pour toute chaîne). probabilistic seulement avec
SPEC=mtp, refusé avec mirostat ou adaptive-p. SPEC_N_MAX → --spec-draft-n-max.
- Jeton de tentative : « loki serve » le pose avant de lancer, le process web
l'efface dès que le moteur répond (sans page ouverte). Resté au lancement
suivant, même preset, moteur et build → auto coupé et dit. Couches poussées
en RAM après chargement : même verdict. Les erreurs MTP/brouillon du journal
ont leur message.
- Éditeur : « auto » et « MTP » écrivent SPEC, « non » écrit SPEC=off ; champ
Brouillon ; la recherche HF propose la tête MTP du dépôt, décochée.
- Tests : table de specArgs (gardes, sauts, brouillons, tirage), place avant
EXTRA_ARGS, cycle du jeton sur une vraie base, lecture du journal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
11 KiB
Go
261 lines
11 KiB
Go
package loki
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Réglages d'expert du moteur local, tous éteints tant que le preset ne les
|
|
// demande pas. Aucun ne touche au modèle : ni poids, ni contexte, ni
|
|
// échantillonnage — seulement OÙ et COMMENT le même calcul s'exécute.
|
|
//
|
|
// FIT_TARGET=1024,3072 → --fit-target : marge VRAM laissée libre par carte
|
|
// OP_OFFLOAD_MIN_BATCH=N → GGML_OP_OFFLOAD_MIN_BATCH : seuil d'envoi au GPU
|
|
// des poids restés en RAM
|
|
// CUDA_GRAPH_OPT=on → GGML_CUDA_GRAPH_OPT=1 : branches Q/K/V en parallèle
|
|
//
|
|
// Une variable déjà présente dans l'environnement (gabarit Docker/Unraid) est
|
|
// un choix de l'utilisateur : jamais touchée. Les variables passent par
|
|
// l'environnement du seul « loki serve », qui devient llama-server : le reste
|
|
// du conteneur ne les voit pas.
|
|
|
|
// expertEnvKeys : les variables du moteur que ces réglages posent.
|
|
var expertEnvKeys = []string{"GGML_CUDA_GRAPH_OPT", "GGML_OP_OFFLOAD_MIN_BATCH"}
|
|
|
|
// fitArgEnv : les LLAMA_ARG_* qui, sans drapeau, décident de --fit et de sa
|
|
// marge. llama.cpp les applique quand la ligne de commande se tait.
|
|
var fitArgEnv = []string{"LLAMA_ARG_FIT", "LLAMA_ARG_FIT_TARGET", "LLAMA_ARG_TENSOR_SPLIT"}
|
|
|
|
// probeExpertEnv relève ce que l'environnement a déjà tranché. Appelée après
|
|
// probeServeGPUs, dont elle complète ArgEnv.
|
|
func probeExpertEnv(si *serveSysInfo) {
|
|
si.UserEnv = map[string]string{}
|
|
for _, k := range expertEnvKeys {
|
|
if v := os.Getenv(k); v != "" {
|
|
si.UserEnv[k] = v
|
|
}
|
|
}
|
|
if si.ArgEnv == nil {
|
|
si.ArgEnv = map[string]string{}
|
|
}
|
|
for _, k := range fitArgEnv {
|
|
if v := os.Getenv(k); v != "" {
|
|
si.ArgEnv[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// fitTargetMin : la marge par défaut de llama.cpp (common.h), en Mio. En
|
|
// dessous, fit range plus serré que ce qu'il sait estimer — le tampon du modèle
|
|
// MTP, qu'il ne mesure pas toujours, suffit alors à ramener le « cudaMalloc
|
|
// failed: out of memory » au premier long prompt. On remonte, en le disant.
|
|
const fitTargetMin = 1024
|
|
|
|
// fitTargetMaxValues : llama.cpp refuse (et le serveur meurt en boucle) une
|
|
// liste aussi longue que son nombre maximal de cartes ; 8 reste loin du bord.
|
|
const fitTargetMaxValues = 8
|
|
|
|
// parseFitTarget valide FIT_TARGET : des entiers en Mio séparés par « , » ou
|
|
// « / », une valeur par carte dans l'ordre que voit le moteur (une seule vaut
|
|
// pour toutes). Jamais transmis brut : llama.cpp lit chaque valeur avec
|
|
// std::stoull, qui lève sur le moindre caractère parasite — moteur mort au
|
|
// démarrage, relancé en boucle par systemd ou Docker.
|
|
func parseFitTarget(v string) (vals []int, raised bool, ok bool) {
|
|
parts := strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == '/' })
|
|
if len(parts) == 0 || len(parts) > fitTargetMaxValues ||
|
|
strings.Count(v, ",")+strings.Count(v, "/") != len(parts)-1 {
|
|
return nil, false, false
|
|
}
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" || strings.Trim(p, "0123456789") != "" || len(p) > 7 {
|
|
return nil, false, false
|
|
}
|
|
n, _ := strconv.Atoi(p)
|
|
if n < fitTargetMin {
|
|
n, raised = fitTargetMin, true
|
|
}
|
|
vals = append(vals, n)
|
|
}
|
|
return vals, raised, true
|
|
}
|
|
|
|
// fitTargetArgs traduit FIT_TARGET en --fit-target. Fonction pure.
|
|
//
|
|
// La marge règle combien de VRAM --fit laisse libre sur chaque carte ; sur deux
|
|
// cartes inégales (5060 Ti + 3060), une marge plus large sur la lente pousse
|
|
// plus de couches vers la rapide. Placement seulement : mêmes poids, mêmes
|
|
// types de cache, même contexte. Rien n'est posé quand :
|
|
//
|
|
// - le moteur ne connaît pas --fit-target (un drapeau inconnu le tue) ;
|
|
// - EXTRA_ARGS ou LLAMA_ARG_FIT_TARGET l'ont déjà fixée ;
|
|
// - le contexte n'est pas un nombre positif : avec -c 0, fit a le droit de
|
|
// RÉDUIRE le contexte pour tenir, et une marge plus large l'y pousserait
|
|
// sans un mot — de l'information perdue pour le modèle ;
|
|
// - --fit ne tournera pas (voir fitBlocker) : la clé ne ferait rien, et une
|
|
// comparaison A/B mesurerait deux fois le même placement.
|
|
func fitTargetArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, notes []string) {
|
|
raw := strings.TrimSpace(cfg["FIT_TARGET"])
|
|
if raw == "" {
|
|
return nil, nil
|
|
}
|
|
ignore := func(why string) (a, n []string) {
|
|
return nil, []string{"FIT_TARGET=" + raw + " ignoré : " + why}
|
|
}
|
|
vals, raised, ok := parseFitTarget(raw)
|
|
switch {
|
|
case !ok:
|
|
return ignore("des Mio par carte, ex. 1024,3072 (8 valeurs au plus)")
|
|
case !strings.Contains(si.Help, "--fit-target"):
|
|
return ignore("ce moteur ne connaît pas --fit-target")
|
|
case hasAnyFlag(extra, "-fitt", "--fit-target"):
|
|
return ignore("--fit-target déjà dans EXTRA_ARGS")
|
|
case si.ArgEnv["LLAMA_ARG_FIT_TARGET"] != "":
|
|
return ignore("LLAMA_ARG_FIT_TARGET déjà posé dans l'environnement")
|
|
}
|
|
if ctx, ok := ctxFixed(cfg, extra); !ok {
|
|
return ignore("contexte « " + ctx + " » non fixé — --fit pourrait le réduire pour tenir ; donne un CTX chiffré")
|
|
}
|
|
if why := fitBlocker(cfg, extra, si); why != "" {
|
|
return ignore("--fit inactif (" + why + "), la marge ne servirait à rien")
|
|
}
|
|
s := make([]string, len(vals))
|
|
for i, n := range vals {
|
|
s[i] = strconv.Itoa(n)
|
|
}
|
|
v := strings.Join(s, ",")
|
|
note := "--fit-target " + v + " (FIT_TARGET, Mio libres par carte, dans l'ordre vu par le moteur) — " +
|
|
"vérifie « offloaded N/N layers » dans le journal"
|
|
if raised {
|
|
note += " ; marges sous 1024 Mio remontées à 1024"
|
|
}
|
|
return []string{"--fit-target", v}, []string{note}
|
|
}
|
|
|
|
// fitBlocker dit pourquoi llama.cpp ne lancera PAS --fit (common/fit.cpp :
|
|
// « already set by user, abort »), ou vide s'il le lancera. Les couches GPU
|
|
// sont lues comme buildServeArgs les écrira : EXTRA_ARGS d'abord, sinon la
|
|
// traduction de NGL par nglArgs (999 y devient auto), sinon — NGL=auto, aucun
|
|
// drapeau — la variable du moteur.
|
|
func fitBlocker(cfg map[string]string, extra []string, si serveSysInfo) string {
|
|
fit := flagValue(extra, "-fit", "--fit")
|
|
if fit == "" {
|
|
fit = si.ArgEnv["LLAMA_ARG_FIT"]
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(fit)) {
|
|
case "off", "disabled", "false", "0":
|
|
return "--fit off"
|
|
}
|
|
ngl := flagValue(extra, "-ngl", "--n-gpu-layers", "--gpu-layers")
|
|
if ngl == "" {
|
|
if a, _ := nglArgs(cfg["NGL"], helpFitsLayersItself(si.Help)); len(a) == 2 {
|
|
ngl = a[1]
|
|
} else {
|
|
ngl = si.ArgEnv["LLAMA_ARG_N_GPU_LAYERS"]
|
|
}
|
|
}
|
|
// -1 est la valeur par défaut du moteur, celle qu'« auto » écrit : fit ne
|
|
// la prend pas pour un choix de l'utilisateur.
|
|
if ngl = strings.TrimSpace(ngl); ngl != "" && ngl != "-1" && !strings.EqualFold(ngl, "auto") {
|
|
return "couches GPU fixées à " + ngl
|
|
}
|
|
if hasAnyFlag(extra, "-ts", "--tensor-split") || si.ArgEnv["LLAMA_ARG_TENSOR_SPLIT"] != "" {
|
|
return "--tensor-split"
|
|
}
|
|
if r := tensorOverride(extra, si.ArgEnv); r != "" {
|
|
return r
|
|
}
|
|
sm := flagValue(extra, "-sm", "--split-mode")
|
|
if sm == "" {
|
|
sm = si.ArgEnv["LLAMA_ARG_SPLIT_MODE"]
|
|
}
|
|
// row ET tensor : fit abandonne sur l'une comme sur l'autre (« not
|
|
// implemented », common/fit.cpp).
|
|
if sm = strings.ToLower(strings.TrimSpace(sm)); sm == "row" || sm == "tensor" {
|
|
return "-sm " + sm
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// opOffloadMinBatchEnv traduit OP_OFFLOAD_MIN_BATCH en GGML_OP_OFFLOAD_MIN_BATCH.
|
|
// Fonction pure.
|
|
//
|
|
// Quand des poids restent en RAM (experts MoE sur CPU), llama.cpp recopie vers
|
|
// le GPU ceux dont un lot a besoin dès qu'il compte au moins 32 jetons ; en
|
|
// dessous, il calcule sur CPU. Monter le seuil garde sur CPU les lots plus
|
|
// courts : utile seulement si une spéculation ngram propose des brouillons de
|
|
// 32 jetons ou plus (les lots de vérification MTP, quelques jetons, restent
|
|
// déjà sous 32). Le prix : un tour d'outil de 32 à N-1 jetons se calcule lui
|
|
// aussi sur CPU. N se trouve à la mesure, pas d'office. Même distribution ;
|
|
// les logits ne sont pas identiques au bit près, comme déjà entre décodage CPU
|
|
// et prefill GPU.
|
|
func opOffloadMinBatchEnv(cfg map[string]string, si serveSysInfo) (val string, notes []string) {
|
|
raw := strings.TrimSpace(cfg["OP_OFFLOAD_MIN_BATCH"])
|
|
if raw == "" || si.UserEnv["GGML_OP_OFFLOAD_MIN_BATCH"] != "" {
|
|
return "", nil
|
|
}
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil || n < 1 || n > 1<<20 {
|
|
return "", []string{"OP_OFFLOAD_MIN_BATCH=" + raw + " ignoré : un nombre de jetons, ex. 128"}
|
|
}
|
|
v := strconv.Itoa(n)
|
|
return v, []string{"GGML_OP_OFFLOAD_MIN_BATCH=" + v + " (OP_OFFLOAD_MIN_BATCH) : lots de moins de " + v +
|
|
" jetons calculés sur CPU pour les poids restés en RAM"}
|
|
}
|
|
|
|
// graphOptEnv traduit CUDA_GRAPH_OPT=on en GGML_CUDA_GRAPH_OPT=1. Fonction pure.
|
|
//
|
|
// Au décodage, llama.cpp lance alors les trois branches Q, K et V de chaque
|
|
// couche sur des flux CUDA concurrents. Chaque opération garde son noyau et ses
|
|
// entrées : sortie identique au jeton près en amont (PR #28198, perplexité
|
|
// inchangée). Gain modeste — quelques % sur un MoE tout GPU, rien sur un dense,
|
|
// dilué quand les experts tournent sur CPU ou que MTP vérifie par lots. Sans
|
|
// effet avant #28198 en multi-GPU ou si les graphes CUDA sont coupés.
|
|
//
|
|
// Expérimental : sur une architecture qu'il n'a pas vue, le repérage des
|
|
// branches peut s'arrêter sur un GGML_ASSERT, et le moteur redémarre en boucle.
|
|
// Une variable ne se détecte pas dans l'aide du moteur : on le dit au
|
|
// lancement, pour que « CUDA_GRAPH_OPT=off puis redémarrer » vienne à l'esprit.
|
|
//
|
|
// Refusé quand Q, K et V d'une même couche pourraient finir sur deux cartes
|
|
// (-sm row/tensor, -ot visant l'attention) : #28198 en fait une condition
|
|
// d'exactitude.
|
|
func graphOptEnv(cfg map[string]string, extra []string, si serveSysInfo) (val string, notes []string) {
|
|
raw := strings.TrimSpace(cfg["CUDA_GRAPH_OPT"])
|
|
switch strings.ToLower(raw) {
|
|
case "", "off":
|
|
return "", nil
|
|
case "on":
|
|
default:
|
|
return "", []string{"CUDA_GRAPH_OPT=" + raw + " ignoré (on ou off)"}
|
|
}
|
|
if si.UserEnv["GGML_CUDA_GRAPH_OPT"] != "" {
|
|
return "", nil
|
|
}
|
|
if why := qkvSplitRisk(extra, si.ArgEnv); why != "" {
|
|
return "", []string{"CUDA_GRAPH_OPT ignoré : " + why + " peut séparer Q, K et V d'une couche entre cartes"}
|
|
}
|
|
return "1", []string{"GGML_CUDA_GRAPH_OPT=1 (CUDA_GRAPH_OPT, expérimental) — si le moteur s'arrête sur un " +
|
|
"GGML_ASSERT, CUDA_GRAPH_OPT=off puis redémarre"}
|
|
}
|
|
|
|
// qkvSplitRisk : un placement qui pourrait répartir les tenseurs d'attention
|
|
// d'une même couche sur plusieurs appareils. La découpe par couches (défaut,
|
|
// --tensor-split compris) garde chaque couche entière : rien à craindre.
|
|
func qkvSplitRisk(extra []string, argEnv map[string]string) string {
|
|
sm := flagValue(extra, "-sm", "--split-mode")
|
|
if sm == "" {
|
|
sm = argEnv["LLAMA_ARG_SPLIT_MODE"]
|
|
}
|
|
if sm = strings.ToLower(strings.TrimSpace(sm)); sm == "row" || sm == "tensor" {
|
|
return "-sm " + sm
|
|
}
|
|
for _, v := range append(flagValues(extra, "-ot", "--override-tensor"), argEnv["LLAMA_ARG_OVERRIDE_TENSOR"]) {
|
|
if strings.Contains(strings.ToLower(v), "attn") {
|
|
return "-ot sur l'attention"
|
|
}
|
|
}
|
|
return ""
|
|
}
|