mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Placement : SPLIT_MODE=tensor, parallélisme de tenseurs entre cartes, en opt-in
En découpe par couches (défaut), chaque carte traverse ses couches à son
tour. En mode tensor, chaque couche est coupée entre les cartes, qui
additionnent ensuite leurs morceaux : décodage plus rapide d'après
l'amont, prefill nettement plus lent. Sur deux GeForce en PCIe et une
boucle d'agent qui relit beaucoup, rien n'est acquis : expérimental,
off par défaut, et une longue liste de refus propres plutôt qu'un
moteur qui meurt en boucle. Sans la clé, ligne et environnement
identiques à l'octet près (test).
- détection : la liste littérale {none,layer,row,tensor} dans l'aide du
moteur — « tensor » seul y est partout (--tensor-split) ; absente =
aucun compagnon posé, note au journal
- GGML_CUDA_ALLREDUCE=internal sauf choix de l'environnement : NCCL
compresse en BF16 les réductions du prefill (avec perte) ; nccl
gardé s'il est posé, avec un avertissement ; note NCCL indisponible /
P2P à poser soi-même
- -ngl all, -ts au prorata de la VRAM totale (--list-devices, ordre du
moteur, --device compris), -fa on : chacun seulement si EXTRA_ARGS ou
une LLAMA_ARG_* ne l'a pas déjà ; CTX explicite exigé
- refus : KV autre que f16/bf16/f32 (KV_TYPE*, -ctk/-ctv, LLAMA_ARG_*,
jamais réécrit), --backend-sampling, -fa off, -sm à la main, poids ou
KV sur CPU, NGL partiel, SPEC, SIDE_SLOT, une seule carte ou non CUDA,
architectures exclues par llama.cpp et qwen4exp, preset externe
- pas de --fit en mode tensor : estimation poids + cache de CTX jetons
contre 90 % de la VRAM, refus au-delà, CTX jamais réduit
- files CUDA, CUDA_GRAPH_OPT et FIT_TARGET voient le -sm tensor ;
SLOT_PERSIST refusé avec la clé ; diagnostic du journal pour les
échecs propres au mode tensor
- README et configTemplate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
539e36ea49
commit
5954d75118
7 files changed
+635
-5
No files matched your search
@@ -561,6 +561,29 @@ Ajoutées par ce fork :
|
|||||||
nature de requête (`kinds.<nature>.draft_rate`) : à garder là où elle paie.
|
nature de requête (`kinds.<nature>.draft_rate`) : à garder là où elle paie.
|
||||||
Les drapeaux d'acceptation synthétique (`--spec-synth-*`, « benchmarking
|
Les drapeaux d'acceptation synthétique (`--spec-synth-*`, « benchmarking
|
||||||
only »), que Loki ne pose jamais, déclenchent un avertissement au lancement.
|
only »), que Loki ne pose jamais, déclenchent un avertissement au lancement.
|
||||||
|
- **Parallélisme de tenseurs** (clé `SPLIT_MODE=tensor`, **off** par défaut,
|
||||||
|
expérimental, moteur relancé) : au lieu de donner à chaque carte des couches
|
||||||
|
entières (`-sm layer`, défaut), chaque couche est coupée entre les cartes,
|
||||||
|
qui additionnent ensuite leurs morceaux. L'amont mesure un décodage plus
|
||||||
|
rapide mais un prefill nettement plus lent : sur deux GeForce en PCIe et une
|
||||||
|
boucle d'agent qui relit beaucoup, à mesurer par tour avant de l'adopter.
|
||||||
|
Loki n'active le mode que si l'aide du moteur liste `{none,layer,row,tensor}`
|
||||||
|
(le mot « tensor » seul est partout), et ajoute alors `-ngl all`, `-ts` au
|
||||||
|
prorata de la VRAM totale des cartes dans l'ordre du moteur (`--device`
|
||||||
|
compris), `-fa on` — chacun seulement s'il n'est pas déjà dans `EXTRA_ARGS`
|
||||||
|
ou une `LLAMA_ARG_*` — et `GGML_CUDA_ALLREDUCE=internal` si l'environnement
|
||||||
|
ne le fixe pas : le chemin NCCL compresse en BF16 les grosses réductions du
|
||||||
|
prefill, une perte de précision ; la réduction interne garde le type natif.
|
||||||
|
« NCCL not compiled in » au journal est alors sans effet ; `GGML_CUDA_P2P=1`
|
||||||
|
reste à poser soi-même si le pilote le permet. Pas de `--fit` en mode
|
||||||
|
tensor : Loki estime poids + cache de `CTX` jetons et refuse au-delà de 90 %
|
||||||
|
de la VRAM — jamais en réduisant `CTX`. Refusé aussi (découpe par couches,
|
||||||
|
note au journal) avec un cache KV autre que f16/bf16/f32 (jamais réécrit
|
||||||
|
d'office), des poids ou le cache KV sur CPU (`--n-cpu-moe`, `-ot`, `NGL`
|
||||||
|
partiel, `-nkvo`), `--backend-sampling`, `-fa off`, un `-sm` déjà réglé,
|
||||||
|
`SPEC`, `SIDE_SLOT`, `CTX` non chiffré, une seule carte ou une carte non
|
||||||
|
CUDA, une architecture que llama.cpp exclut (et `qwen4exp`), un preset
|
||||||
|
externe ; `SLOT_PERSIST` est refusé avec la clé.
|
||||||
- **Discussions multiples** : historique complet dans la barre latérale, titre
|
- **Discussions multiples** : historique complet dans la barre latérale, titre
|
||||||
repris du premier message (renommable), suppression. **Chaque discussion a son
|
repris du premier message (renommable), suppression. **Chaque discussion a son
|
||||||
dossier de fichiers** (`workspace/discussions/<id>/`) : les pièces jointes
|
dossier de fichiers** (`workspace/discussions/<id>/`) : les pièces jointes
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ func cmdServe(args []string) error {
|
|||||||
probeCacheRAM(cfg, extra, &si)
|
probeCacheRAM(cfg, extra, &si)
|
||||||
probeCkptEnv(&si)
|
probeCkptEnv(&si)
|
||||||
probeSideSlotEnv(cfg, &si)
|
probeSideSlotEnv(cfg, &si)
|
||||||
|
// SPLIT_MODE=tensor seulement : cartes listées par CE moteur, dans son ordre.
|
||||||
|
probeSplit(cfg, bin, &si)
|
||||||
spec := specMode(cfg)
|
spec := specMode(cfg)
|
||||||
if spec != "off" && spec != "" {
|
if spec != "off" && spec != "" {
|
||||||
probeSpec(cfg, &si)
|
probeSpec(cfg, &si)
|
||||||
@@ -455,6 +457,10 @@ type serveSysInfo struct {
|
|||||||
DraftErr string // pourquoi MODEL_DRAFT n'a pas été trouvé
|
DraftErr string // pourquoi MODEL_DRAFT n'a pas été trouvé
|
||||||
DraftGGUF *GGUFInfo // métadonnées du brouillon ; nil = illisibles
|
DraftGGUF *GGUFInfo // métadonnées du brouillon ; nil = illisibles
|
||||||
SpecAutoBlocked string // un essai automatique précédent a échoué : la raison
|
SpecAutoBlocked string // un essai automatique précédent a échoué : la raison
|
||||||
|
|
||||||
|
// Cartes listées par le moteur (--list-devices), lues seulement pour
|
||||||
|
// SPLIT_MODE=tensor (voir backend_serve_split.go) ; vide = inconnues.
|
||||||
|
SplitDevs []splitDev
|
||||||
}
|
}
|
||||||
|
|
||||||
// cudaDeviceEnv : sélection GPU (loki gpu), on filtre les devices visibles par
|
// cudaDeviceEnv : sélection GPU (loki gpu), on filtre les devices visibles par
|
||||||
@@ -516,16 +522,30 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
|
|||||||
notes = append(notes, loadNote)
|
notes = append(notes, loadNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parallélisme de tenseurs (SPLIT_MODE=tensor, voir backend_serve_split.go) :
|
||||||
|
// sans la clé, splitOn est faux et rien de ce qui suit ne change. Accepté, il
|
||||||
|
// compte pour les réglages qui lisent le placement dans EXTRA_ARGS (files de
|
||||||
|
// lancement, graphes CUDA, --fit) comme un -sm tensor écrit à la main.
|
||||||
|
splitOn, splitA, splitNGLArgs, splitEnv, splitNotes := splitTensorArgs(cfg, extra, si)
|
||||||
|
place := extra
|
||||||
|
if splitOn {
|
||||||
|
place = append(append([]string{}, extra...), "-sm", "tensor")
|
||||||
|
}
|
||||||
|
for k, v := range splitEnv {
|
||||||
|
env[k] = v
|
||||||
|
}
|
||||||
|
notes = append(notes, splitNotes...)
|
||||||
|
|
||||||
// Files de lancement CUDA : une variable d'environnement, pas un drapeau —
|
// Files de lancement CUDA : une variable d'environnement, pas un drapeau —
|
||||||
// la ligne de commande n'en dépend pas (voir launchQueuesEnv).
|
// la ligne de commande n'en dépend pas (voir launchQueuesEnv).
|
||||||
q, qNotes := launchQueuesEnv(cfg, extra, si)
|
q, qNotes := launchQueuesEnv(cfg, place, si)
|
||||||
if q != "" {
|
if q != "" {
|
||||||
env["CUDA_SCALE_LAUNCH_QUEUES"] = q
|
env["CUDA_SCALE_LAUNCH_QUEUES"] = q
|
||||||
}
|
}
|
||||||
notes = append(notes, qNotes...)
|
notes = append(notes, qNotes...)
|
||||||
// Réglages d'expert passés par variable (voir backend_serve_expert.go) :
|
// Réglages d'expert passés par variable (voir backend_serve_expert.go) :
|
||||||
// absents du preset, ils ne posent rien.
|
// absents du preset, ils ne posent rien.
|
||||||
g, gNotes := graphOptEnv(cfg, extra, si)
|
g, gNotes := graphOptEnv(cfg, place, si)
|
||||||
if g != "" {
|
if g != "" {
|
||||||
env["GGML_CUDA_GRAPH_OPT"] = g
|
env["GGML_CUDA_GRAPH_OPT"] = g
|
||||||
}
|
}
|
||||||
@@ -595,7 +615,9 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
|
|||||||
// ci-dessus. Sur un moteur qui connaît « auto », 999 devient donc auto, et on
|
// ci-dessus. Sur un moteur qui connaît « auto », 999 devient donc auto, et on
|
||||||
// le DIT sur stderr plutôt que de le faire en douce. Qui veut réellement
|
// le DIT sur stderr plutôt que de le faire en douce. Qui veut réellement
|
||||||
// forcer tout sur le GPU écrit NGL=all (ou un nombre qui n'est pas 999).
|
// forcer tout sur le GPU écrit NGL=all (ou un nombre qui n'est pas 999).
|
||||||
if !hasAnyFlag(extra, "-ngl", "--n-gpu-layers", "--gpu-layers") {
|
if splitOn {
|
||||||
|
args = append(args, splitNGLArgs...) // « -ngl all », ou rien si EXTRA_ARGS l'a déjà
|
||||||
|
} else if !hasAnyFlag(extra, "-ngl", "--n-gpu-layers", "--gpu-layers") {
|
||||||
ngl, note := nglArgs(get("NGL", ""), helpFitsLayersItself(si.Help))
|
ngl, note := nglArgs(get("NGL", ""), helpFitsLayersItself(si.Help))
|
||||||
if note != "" {
|
if note != "" {
|
||||||
notes = append(notes, note)
|
notes = append(notes, note)
|
||||||
@@ -604,8 +626,8 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
|
|||||||
}
|
}
|
||||||
// Marge VRAM par carte du placement automatique (FIT_TARGET, voir
|
// Marge VRAM par carte du placement automatique (FIT_TARGET, voir
|
||||||
// fitTargetArgs) : seulement si --fit tournera vraiment.
|
// fitTargetArgs) : seulement si --fit tournera vraiment.
|
||||||
fitt, fitNotes := fitTargetArgs(cfg, extra, si)
|
fitt, fitNotes := fitTargetArgs(cfg, place, si)
|
||||||
args = append(args, fitt...)
|
args = append(append(args, fitt...), splitA...)
|
||||||
notes = append(notes, fitNotes...)
|
notes = append(notes, fitNotes...)
|
||||||
if ktv != "" {
|
if ktv != "" {
|
||||||
args = append(args, "-ctk", ktv)
|
args = append(args, "-ctk", ktv)
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ func slotPersistPlan(cfg map[string]string, extra []string, si serveSysInfo) (ok
|
|||||||
return false, "dossier des slots indisponible"
|
return false, "dossier des slots indisponible"
|
||||||
case specMode(cfg) != "off" || specUserSet(extra, si.ArgEnv) || si.ArgEnv["LLAMA_ARG_SPEC_TYPE"] != "":
|
case specMode(cfg) != "off" || specUserSet(extra, si.ArgEnv) || si.ArgEnv["LLAMA_ARG_SPEC_TYPE"] != "":
|
||||||
return false, "décodage spéculatif : la sauvegarde du moteur ne garde pas l'état du brouillon"
|
return false, "décodage spéculatif : la sauvegarde du moteur ne garde pas l'état du brouillon"
|
||||||
|
case splitModeKey(cfg) != "":
|
||||||
|
return false, "SPLIT_MODE : état réparti entre cartes, sauvegarde non validée"
|
||||||
}
|
}
|
||||||
if !slotSaveSafe(cfg, extra, si) {
|
if !slotSaveSafe(cfg, extra, si) {
|
||||||
return false, "moteur joignable par d'autres sans clé d'API (HOST=127.0.0.1 ou une clé d'API)"
|
return false, "moteur joignable par d'autres sans clé d'API (HOST=127.0.0.1 ou une clé d'API)"
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parallélisme de tenseurs (SPLIT_MODE=tensor, off par défaut) : expérimental.
|
||||||
|
//
|
||||||
|
// La découpe par couches (défaut de llama.cpp) donne à chaque carte des
|
||||||
|
// couches entières, traversées l'une après l'autre. En mode tensor, chaque
|
||||||
|
// couche est coupée entre les cartes, qui calculent en même temps puis
|
||||||
|
// additionnent leurs morceaux (all-reduce). Sur deux RTX 4090, l'amont mesure
|
||||||
|
// +21-30 % au décodage, mais -37 % au prefill — et la boucle d'agent de Loki
|
||||||
|
// relit beaucoup. Sur deux GeForce en PCIe sans P2P, la somme passe par
|
||||||
|
// l'hôte : un gain net par tour est loin d'être acquis. D'où l'opt-in, et une
|
||||||
|
// longue liste de refus propres plutôt qu'un moteur qui meurt en boucle.
|
||||||
|
//
|
||||||
|
// Fidélité : l'all-reduce change l'ordre des additions (bruit de virgule
|
||||||
|
// flottante), mais le chemin NCCL va plus loin — au-delà de 32 768 éléments il
|
||||||
|
// COMPRESSE les sommes partielles en BF16 (ggml-cuda.cu), soit à chaque couche
|
||||||
|
// de chaque prefill : une perte de précision du calcul, de la classe d'une
|
||||||
|
// quantification. Loki pose donc GGML_CUDA_ALLREDUCE=internal (réduction dans
|
||||||
|
// le type natif), sauf si l'utilisateur a choisi lui-même. Le cache KV doit
|
||||||
|
// rester f16, bf16 ou f32 : le mode tensor ne gère pas les caches quantifiés,
|
||||||
|
// et Loki ne réécrit jamais un type de cache d'office (ça change le budget
|
||||||
|
// VRAM). Pas de --fit en mode tensor (common/fit.cpp) : Loki estime la VRAM
|
||||||
|
// lui-même et refuse si le compte ne tient pas, sans jamais réduire CTX.
|
||||||
|
|
||||||
|
// splitDev : une carte telle que la voit CE moteur (--list-devices), dans son
|
||||||
|
// ordre — celui que suivent -ts et --device.
|
||||||
|
type splitDev struct {
|
||||||
|
ID string // CUDA0, Vulkan1…
|
||||||
|
TotalMiB int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitTensorDenyArch : les architectures que llama.cpp refuse en mode tensor
|
||||||
|
// (llm_arch_supports_sm_tensor) — il mourrait au chargement, et systemd
|
||||||
|
// relancerait en boucle. qwen4exp n'y est pas, mais Loki l'écarte : son preset
|
||||||
|
// place les experts sur CPU avec un cache q8_0, l'inverse de ce qu'exige le
|
||||||
|
// mode tensor.
|
||||||
|
var splitTensorDenyArch = map[string]bool{
|
||||||
|
"grok": true, "mpt": true, "plamo2": true, "minicpm3": true, "gemma3n": true, "mamba": true, "mamba2": true,
|
||||||
|
"jamba": true, "falcon-h1": true, "olmo2": true, "olmoe": true, "deepseek2": true, "deepseek32": true,
|
||||||
|
"hy_v4": true, "dots3note": true, "glm-dsa": true, "bitnet": true, "t5": true, "nemotron_h": true,
|
||||||
|
"nemotron_h_moe": true, "granitehybrid": true, "minimax-01": true, "minimax-m2": true, "minimax-m3": true,
|
||||||
|
"mistral4": true, "kimi-linear": true, "bailingmoe3": true, "kimi-k3": true, "glm5-next": true,
|
||||||
|
"qwen3tts": true, "qwen4exp": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitArgEnv : les variables qui décident de ce que SPLIT_MODE règlerait.
|
||||||
|
var splitArgEnv = []string{"LLAMA_ARG_FLASH_ATTN", "LLAMA_ARG_BACKEND_SAMPLING", "LLAMA_ARG_TENSOR_SPLIT"}
|
||||||
|
|
||||||
|
// splitModeKey lit SPLIT_MODE : "" (clé absente, off ou layer — le défaut du
|
||||||
|
// moteur, rien à poser), "tensor", ou "?" si illisible.
|
||||||
|
func splitModeKey(cfg map[string]string) string {
|
||||||
|
switch v := strings.ToLower(strings.TrimSpace(cfg["SPLIT_MODE"])); v {
|
||||||
|
case "", "off", "layer", "non", "no", "0":
|
||||||
|
return ""
|
||||||
|
case "tensor":
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpSupportsSplitTensor : la liste LITTÉRALE des valeurs de -sm. Le mot
|
||||||
|
// « tensor » seul est partout dans l'aide (--tensor-split, --override-tensor) :
|
||||||
|
// s'y fier ferait passer -sm tensor à un moteur qui le refuse au démarrage.
|
||||||
|
func helpSupportsSplitTensor(help string) bool {
|
||||||
|
return strings.Contains(help, "{none,layer,row,tensor}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// probeSplit : seulement si SPLIT_MODE=tensor et que le moteur le propose —
|
||||||
|
// sans la clé, rien n'est lu ni lancé. --list-devices tourne dans
|
||||||
|
// l'environnement du vrai lancement (bibliothèques, CUDA_VISIBLE_DEVICES et
|
||||||
|
// PCI_BUS_ID déjà posés par cmdServe) : même ordre de cartes que le moteur.
|
||||||
|
func probeSplit(cfg map[string]string, bin string, si *serveSysInfo) {
|
||||||
|
if splitModeKey(cfg) != "tensor" || !helpSupportsSplitTensor(si.Help) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if si.ArgEnv == nil {
|
||||||
|
si.ArgEnv = map[string]string{}
|
||||||
|
}
|
||||||
|
for _, k := range splitArgEnv {
|
||||||
|
if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
|
||||||
|
si.ArgEnv[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if si.UserEnv == nil {
|
||||||
|
si.UserEnv = map[string]string{}
|
||||||
|
}
|
||||||
|
if v := os.Getenv("GGML_CUDA_ALLREDUCE"); v != "" {
|
||||||
|
si.UserEnv["GGML_CUDA_ALLREDUCE"] = v
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out, err := hideCmd(exec.CommandContext(ctx, bin, "--list-devices")).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return // sortie tronquée (carte pleine, moteur planté) : on ne s'y fie pas
|
||||||
|
}
|
||||||
|
for _, d := range parseListDevices(string(out)) {
|
||||||
|
id, _ := d["id"].(string)
|
||||||
|
total, _ := d["total_mib"].(int)
|
||||||
|
si.SplitDevs = append(si.SplitDevs, splitDev{ID: id, TotalMiB: int64(total)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPlan décide de SPLIT_MODE=tensor pour ce lancement. Fonction pure.
|
||||||
|
//
|
||||||
|
// on : accepté ; la ligne gagne -sm tensor et ses compagnons
|
||||||
|
// why : pourquoi refusé (vide = clé absente)
|
||||||
|
// devs : les cartes du découpage, dans l'ordre du moteur
|
||||||
|
// needMiB : la VRAM estimée (poids + cache de CTX jetons)
|
||||||
|
func splitPlan(cfg map[string]string, extra []string, si serveSysInfo) (on bool, why string, devs []splitDev, needMiB int64) {
|
||||||
|
switch splitModeKey(cfg) {
|
||||||
|
case "":
|
||||||
|
return false, "", nil, 0
|
||||||
|
case "?":
|
||||||
|
return false, "SPLIT_MODE=" + strings.TrimSpace(cfg["SPLIT_MODE"]) + " illisible (tensor ou off)", nil, 0
|
||||||
|
}
|
||||||
|
refuse := func(why string) (bool, string, []splitDev, int64) { return false, why, nil, 0 }
|
||||||
|
if isExternalConfig(cfg) {
|
||||||
|
return refuse("preset externe")
|
||||||
|
}
|
||||||
|
if !helpSupportsSplitTensor(si.Help) {
|
||||||
|
return refuse("ce moteur ne propose pas -sm tensor")
|
||||||
|
}
|
||||||
|
if sm := flagValue(extra, "-sm", "--split-mode"); sm != "" {
|
||||||
|
return refuse("-sm " + sm + " déjà dans EXTRA_ARGS")
|
||||||
|
}
|
||||||
|
if si.ArgEnv["LLAMA_ARG_SPLIT_MODE"] != "" {
|
||||||
|
return refuse("LLAMA_ARG_SPLIT_MODE déjà posée")
|
||||||
|
}
|
||||||
|
fa := flagValue(extra, "-fa", "--flash-attn")
|
||||||
|
if fa == "" {
|
||||||
|
fa = si.ArgEnv["LLAMA_ARG_FLASH_ATTN"]
|
||||||
|
}
|
||||||
|
switch strings.ToLower(strings.TrimSpace(fa)) {
|
||||||
|
case "off", "disabled", "false", "0":
|
||||||
|
return refuse("Flash Attention coupée (-fa " + fa + ") : le mode tensor l'exige")
|
||||||
|
}
|
||||||
|
if hasAnyFlag(extra, "-bs", "--backend-sampling") || envTruthy(si.ArgEnv["LLAMA_ARG_BACKEND_SAMPLING"]) {
|
||||||
|
return refuse("échantillonnage sur GPU (--backend-sampling) non pris en charge en mode tensor")
|
||||||
|
}
|
||||||
|
k, v, _ := effectiveKVTypes(cfg, extra, si.ArgEnv)
|
||||||
|
if !splitKVOK(k) || !splitKVOK(v) {
|
||||||
|
return refuse(fmt.Sprintf("cache KV %s/%s : le mode tensor n'accepte que f16, bf16 ou f32 "+
|
||||||
|
"(jamais réécrit d'office : la VRAM changerait)", kvName(k), kvName(v)))
|
||||||
|
}
|
||||||
|
if r := tensorOverride(extra, si.ArgEnv); r != "" {
|
||||||
|
return refuse(r + " : le mode tensor met tout sur les cartes")
|
||||||
|
}
|
||||||
|
if !kvOffloaded(extra, si.ArgEnv) {
|
||||||
|
return refuse("cache KV sur CPU")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case si.GGUF == nil || si.ModelBytes <= 0:
|
||||||
|
return refuse("métadonnées ou taille du modèle illisibles")
|
||||||
|
case splitTensorDenyArch[strings.ToLower(si.GGUF.Arch)]:
|
||||||
|
return refuse("architecture " + si.GGUF.Arch + " exclue du mode tensor")
|
||||||
|
}
|
||||||
|
if _, why := splitNGL(cfg, extra, si.GGUF.BlockCount); why != "" {
|
||||||
|
return refuse(why)
|
||||||
|
}
|
||||||
|
ctxS, ok := ctxFixed(cfg, extra)
|
||||||
|
if !ok {
|
||||||
|
return refuse("contexte « " + ctxS + " » non chiffré : pas de --fit en mode tensor, donne un CTX explicite")
|
||||||
|
}
|
||||||
|
if sideSlotOn(cfg) {
|
||||||
|
return refuse("SIDE_SLOT : cache doublé, sans --fit pour rattraper")
|
||||||
|
}
|
||||||
|
if specMode(cfg) != "off" || specUserSet(extra, si.ArgEnv) {
|
||||||
|
return refuse("décodage spéculatif : brouillon et vérification par lots non validés en mode tensor")
|
||||||
|
}
|
||||||
|
devs, why = splitDevices(extra, si)
|
||||||
|
if why != "" {
|
||||||
|
return refuse(why)
|
||||||
|
}
|
||||||
|
// VRAM : poids + cache de CTX jetons (et état récurrent) contre 90 % des
|
||||||
|
// cartes, la marge de SIDE_SLOT. Les tampons de calcul n'y sont pas : c'est
|
||||||
|
// un minimum, pas une promesse — le journal dira le reste.
|
||||||
|
ctx, _ := strconv.Atoi(ctxS)
|
||||||
|
perTok, rec := ggufStateBytes(*si.GGUF, k, v)
|
||||||
|
if perTok <= 0 {
|
||||||
|
return refuse("taille du cache KV inconnue pour ce modèle")
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
for _, d := range devs {
|
||||||
|
total += d.TotalMiB
|
||||||
|
}
|
||||||
|
mm := int64(0)
|
||||||
|
if si.MMProj != "" {
|
||||||
|
if st, err := os.Stat(si.MMProj); err == nil {
|
||||||
|
mm = st.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
need := float64(si.ModelBytes+mm) + perTok*float64(ctx) + float64(rec)
|
||||||
|
needMiB = int64(math.Ceil(need / (1 << 20)))
|
||||||
|
if need > 0.9*float64(total)*(1<<20) {
|
||||||
|
return refuse(fmt.Sprintf("VRAM insuffisante sans --fit : ~%d Mio (poids + cache de %d jetons) pour %d Mio "+
|
||||||
|
"sur %d cartes — CTX n'est jamais réduit d'office", needMiB, ctx, total, len(devs)))
|
||||||
|
}
|
||||||
|
return true, "", devs, needMiB
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitKVOK(t string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(t)) {
|
||||||
|
case "", "f16", "bf16", "f32":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func kvName(t string) string {
|
||||||
|
if t == "" {
|
||||||
|
return "f16"
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitNGL : les couches GPU en mode tensor, où tout doit être sur les cartes.
|
||||||
|
// Un -ngl d'EXTRA_ARGS reste celui de l'utilisateur (rien à ajouter) s'il dit
|
||||||
|
// « tout » ; sinon NGL est traduit en « -ngl all » (999, auto et la clé absente
|
||||||
|
// visent déjà le tout-GPU, mais auto ne fait rien sans --fit). Une valeur qui
|
||||||
|
// laisse des couches au CPU est refusée.
|
||||||
|
func splitNGL(cfg map[string]string, extra []string, blocks int) (args []string, why string) {
|
||||||
|
all := func(v string) bool {
|
||||||
|
v = strings.ToLower(strings.TrimSpace(v))
|
||||||
|
if v == "all" || v == "999" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
return err == nil && blocks > 0 && n >= blocks+1
|
||||||
|
}
|
||||||
|
if v := flagValue(extra, "-ngl", "--n-gpu-layers", "--gpu-layers"); v != "" {
|
||||||
|
if all(v) {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
return nil, "-ngl " + v + " dans EXTRA_ARGS : le mode tensor veut toutes les couches sur les cartes (all)"
|
||||||
|
}
|
||||||
|
switch v := strings.TrimSpace(cfg["NGL"]); {
|
||||||
|
case v == "" || strings.EqualFold(v, "auto") || all(v):
|
||||||
|
return []string{"-ngl", "all"}, ""
|
||||||
|
default:
|
||||||
|
return nil, "NGL=" + v + " : le mode tensor veut toutes les couches sur les cartes (all)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitDevices : les cartes du découpage. --device (EXTRA_ARGS ou variable)
|
||||||
|
// choisit lesquelles, dans son ordre ; sinon toutes celles que liste le
|
||||||
|
// moteur. Toutes CUDA (l'all-reduce n'existe qu'entre cartes CUDA), deux au
|
||||||
|
// moins.
|
||||||
|
func splitDevices(extra []string, si serveSysInfo) ([]splitDev, string) {
|
||||||
|
if len(si.SplitDevs) == 0 {
|
||||||
|
return nil, "cartes inconnues (--list-devices n'a rien répondu)"
|
||||||
|
}
|
||||||
|
devs := si.SplitDevs
|
||||||
|
sel := flagValue(extra, "-dev", "--device")
|
||||||
|
if sel == "" {
|
||||||
|
sel = si.ArgEnv["LLAMA_ARG_DEVICE"]
|
||||||
|
}
|
||||||
|
if sel = strings.TrimSpace(sel); sel != "" {
|
||||||
|
devs = nil
|
||||||
|
for _, name := range strings.Split(sel, ",") {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
found := false
|
||||||
|
for _, d := range si.SplitDevs {
|
||||||
|
if strings.EqualFold(d.ID, name) {
|
||||||
|
devs, found = append(devs, d), true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, "--device " + name + " inconnu de ce moteur"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, d := range devs {
|
||||||
|
if !strings.HasPrefix(strings.ToUpper(d.ID), "CUDA") {
|
||||||
|
return nil, d.ID + " n'est pas une carte CUDA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(devs) < 2 {
|
||||||
|
return nil, "une seule carte"
|
||||||
|
}
|
||||||
|
return devs, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitTS : la répartition par défaut, au prorata de la VRAM TOTALE de chaque
|
||||||
|
// carte (la libre varie avec ce qui tourne encore au lancement), en pour cent.
|
||||||
|
func splitTS(devs []splitDev) string {
|
||||||
|
var total int64
|
||||||
|
for _, d := range devs {
|
||||||
|
total += d.TotalMiB
|
||||||
|
}
|
||||||
|
if total <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts := make([]string, len(devs))
|
||||||
|
for i, d := range devs {
|
||||||
|
parts[i] = strconv.FormatInt(int64(math.Round(100*float64(d.TotalMiB)/float64(total))), 10)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitTensorArgs : ce que SPLIT_MODE=tensor ajoute à la ligne, à la place du -ngl
|
||||||
|
// habituel (nglOut), l'environnement et les notes. on=false : rien, et la
|
||||||
|
// ligne reste celle d'avant à l'octet près.
|
||||||
|
func splitTensorArgs(cfg map[string]string, extra []string, si serveSysInfo) (on bool, args, ngl []string, env map[string]string, notes []string) {
|
||||||
|
ok, why, devs, needMiB := splitPlan(cfg, extra, si)
|
||||||
|
if !ok {
|
||||||
|
if why != "" {
|
||||||
|
notes = append(notes, "SPLIT_MODE=tensor refusé ("+why+") : découpe par couches, comme sans la clé")
|
||||||
|
}
|
||||||
|
return false, nil, nil, nil, notes
|
||||||
|
}
|
||||||
|
ngl, _ = splitNGL(cfg, extra, si.GGUF.BlockCount)
|
||||||
|
args = []string{"-sm", "tensor"}
|
||||||
|
ts := ""
|
||||||
|
if !hasAnyFlag(extra, "-ts", "--tensor-split") && si.ArgEnv["LLAMA_ARG_TENSOR_SPLIT"] == "" {
|
||||||
|
if ts = splitTS(devs); ts != "" {
|
||||||
|
args = append(args, "-ts", ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// llama.cpp active de lui-même Flash Attention en mode tensor ; l'écrire
|
||||||
|
// le rend lisible dans le journal. Jamais en double d'un -fa de l'utilisateur.
|
||||||
|
if !hasAnyFlag(extra, "-fa", "--flash-attn") && si.ArgEnv["LLAMA_ARG_FLASH_ATTN"] == "" {
|
||||||
|
args = append(args, "-fa", "on")
|
||||||
|
}
|
||||||
|
env = map[string]string{}
|
||||||
|
note := fmt.Sprintf("SPLIT_MODE=tensor (expérimental) : -sm tensor sur %d cartes", len(devs))
|
||||||
|
if ts != "" {
|
||||||
|
note += ", -ts " + ts + " (VRAM totale)"
|
||||||
|
}
|
||||||
|
note += fmt.Sprintf(", ~%d Mio estimés sans --fit ; décodage plus rapide, prefill plus lent — compare le temps par tour", needMiB)
|
||||||
|
switch ar := strings.ToLower(strings.TrimSpace(si.UserEnv["GGML_CUDA_ALLREDUCE"])); ar {
|
||||||
|
case "":
|
||||||
|
env["GGML_CUDA_ALLREDUCE"] = "internal"
|
||||||
|
note += " ; GGML_CUDA_ALLREDUCE=internal : sommes dans le type natif (NCCL les compresserait en BF16, avec perte). " +
|
||||||
|
"« NCCL not compiled in » ou « NCCL is unavailable » au journal : sans effet ici. GeForce sans P2P : les " +
|
||||||
|
"sommes passent par l'hôte ; GGML_CUDA_P2P=1 à poser soi-même si le pilote le permet"
|
||||||
|
case "nccl":
|
||||||
|
note += " ; avertissement : GGML_CUDA_ALLREDUCE=nccl (environnement) — NCCL compresse en BF16 les grosses " +
|
||||||
|
"réductions du prefill : calcul avec perte. « NCCL not compiled in » au journal = repli sur la réduction interne"
|
||||||
|
default:
|
||||||
|
note += " ; GGML_CUDA_ALLREDUCE=" + ar + " (environnement) gardé"
|
||||||
|
}
|
||||||
|
if si.GGUF.Hybrid {
|
||||||
|
note += " ; modèle hybride : état récurrent et points de reprise répartis entre cartes, non validés en amont"
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
return true, args, ngl, env, notes
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const helpTensor = helpRecent + `-sm, --split-mode {none,layer,row,tensor}
|
||||||
|
how to split the model across multiple GPUs
|
||||||
|
-ts, --tensor-split N0,N1,N2,... fraction of the model to offload to each GPU
|
||||||
|
`
|
||||||
|
|
||||||
|
// tensorReady : un hybride 64 couches de 16 Gio sur 16 + 12 Gio de VRAM, sur
|
||||||
|
// un moteur qui propose -sm tensor. Le cas où SPLIT_MODE=tensor est accepté.
|
||||||
|
func tensorReady() serveSysInfo {
|
||||||
|
return serveSysInfo{
|
||||||
|
Help: helpTensor,
|
||||||
|
Model: "/models/Qwen3.6-27B.gguf",
|
||||||
|
GGUF: &GGUFInfo{Arch: "qwen35", BlockCount: 64, KeyLen: 256, ValLen: 256, HeadCountKV: 4,
|
||||||
|
Hybrid: true, FullAttnInterval: 4},
|
||||||
|
ModelBytes: 16 << 30,
|
||||||
|
ArgEnv: map[string]string{},
|
||||||
|
UserEnv: map[string]string{},
|
||||||
|
SplitDevs: []splitDev{{ID: "CUDA0", TotalMiB: 16311}, {ID: "CUDA1", TotalMiB: 12288}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitTensorPlan(t *testing.T) {
|
||||||
|
tensor := map[string]string{"SPLIT_MODE": "tensor", "CTX": "65536"}
|
||||||
|
with := func(kv ...string) map[string]string {
|
||||||
|
m := map[string]string{}
|
||||||
|
for k, v := range tensor {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
for i := 0; i+1 < len(kv); i += 2 {
|
||||||
|
m[kv[i]] = kv[i+1]
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
cfg map[string]string
|
||||||
|
extra []string
|
||||||
|
si func(*serveSysInfo)
|
||||||
|
on bool
|
||||||
|
why string // fragment attendu de la raison du refus
|
||||||
|
}{
|
||||||
|
{name: "clé absente : rien, en silence", cfg: map[string]string{}},
|
||||||
|
{name: "SPLIT_MODE=layer : le défaut, rien", cfg: map[string]string{"SPLIT_MODE": "layer"}},
|
||||||
|
{name: "accepté", cfg: tensor, on: true},
|
||||||
|
{name: "illisible", cfg: with("SPLIT_MODE", "rows"), why: "illisible"},
|
||||||
|
{name: "aide qui ne cite que --tensor-split : refusé",
|
||||||
|
cfg: tensor, why: "-sm tensor", si: func(s *serveSysInfo) { s.Help = helpRecent + "-ts, --tensor-split N0,N1\n" }},
|
||||||
|
{name: "preset externe", cfg: with(extKeyFlag, "1"), why: "externe"},
|
||||||
|
{name: "-sm dans EXTRA_ARGS : l'utilisateur garde la main", cfg: tensor, extra: []string{"-sm", "layer"}, why: "-sm layer"},
|
||||||
|
{name: "LLAMA_ARG_SPLIT_MODE", cfg: tensor, why: "LLAMA_ARG_SPLIT_MODE",
|
||||||
|
si: func(s *serveSysInfo) { s.ArgEnv["LLAMA_ARG_SPLIT_MODE"] = "row" }},
|
||||||
|
{name: "-fa off : refusé", cfg: tensor, extra: []string{"-fa", "off"}, why: "Flash Attention"},
|
||||||
|
{name: "-fa on : accepté", cfg: tensor, extra: []string{"-fa", "on"}, on: true},
|
||||||
|
{name: "--backend-sampling : refusé", cfg: tensor, extra: []string{"--backend-sampling"}, why: "backend-sampling"},
|
||||||
|
{name: "LLAMA_ARG_BACKEND_SAMPLING : refusé", cfg: tensor, why: "backend-sampling",
|
||||||
|
si: func(s *serveSysInfo) { s.ArgEnv["LLAMA_ARG_BACKEND_SAMPLING"] = "1" }},
|
||||||
|
{name: "KV_TYPE=q8_0 : refusé, jamais réécrit", cfg: with("KV_TYPE", "q8_0"), why: "q8_0"},
|
||||||
|
{name: "-ctk q8_0 dans EXTRA_ARGS (preset MoE) : refusé", cfg: tensor,
|
||||||
|
extra: []string{"--cache-type-k", "q8_0", "--cache-type-v", "q8_0"}, why: "q8_0"},
|
||||||
|
{name: "KV_TYPE_V=q4_0 : refusé", cfg: with("KV_TYPE_V", "q4_0"), why: "q4_0"},
|
||||||
|
{name: "KV bf16 : accepté", cfg: with("KV_TYPE", "bf16"), on: true},
|
||||||
|
{name: "--n-cpu-moe : refusé", cfg: tensor, extra: []string{"--n-cpu-moe", "40"}, why: "n-cpu-moe"},
|
||||||
|
{name: "-ot : refusé", cfg: tensor, extra: []string{"-ot", "exps=CPU"}, why: "surcharge"},
|
||||||
|
{name: "cache KV sur CPU : refusé", cfg: tensor, extra: []string{"-nkvo"}, why: "KV sur CPU"},
|
||||||
|
{name: "NGL=40 : refusé", cfg: with("NGL", "40"), why: "NGL=40"},
|
||||||
|
{name: "NGL=999 : accepté", cfg: with("NGL", "999"), on: true},
|
||||||
|
{name: "-ngl 99 dans EXTRA_ARGS (≥ toutes les couches) : accepté", cfg: tensor, extra: []string{"-ngl", "99"}, on: true},
|
||||||
|
{name: "-ngl 30 dans EXTRA_ARGS : refusé", cfg: tensor, extra: []string{"-ngl", "30"}, why: "-ngl 30"},
|
||||||
|
{name: "CTX=0 : refusé", cfg: with("CTX", "0"), why: "non chiffré"},
|
||||||
|
{name: "VRAM insuffisante : refusé, CTX jamais réduit", cfg: with("CTX", "262144"), why: "VRAM insuffisante"},
|
||||||
|
{name: "qwen4exp : refusé", cfg: tensor, why: "qwen4exp", si: func(s *serveSysInfo) { s.GGUF.Arch = "qwen4exp" }},
|
||||||
|
{name: "mamba (refusé par llama.cpp) : refusé proprement", cfg: tensor, why: "mamba",
|
||||||
|
si: func(s *serveSysInfo) { s.GGUF.Arch = "mamba" }},
|
||||||
|
{name: "GGUF illisible", cfg: tensor, why: "illisibles", si: func(s *serveSysInfo) { s.GGUF = nil }},
|
||||||
|
{name: "SPEC=mtp : refusé", cfg: with("SPEC", "mtp"), why: "spéculatif"},
|
||||||
|
{name: "SIDE_SLOT=on : refusé", cfg: with("SIDE_SLOT", "on"), why: "SIDE_SLOT"},
|
||||||
|
{name: "une seule carte", cfg: tensor, why: "une seule carte",
|
||||||
|
si: func(s *serveSysInfo) { s.SplitDevs = s.SplitDevs[:1] }},
|
||||||
|
{name: "cartes inconnues", cfg: tensor, why: "inconnues", si: func(s *serveSysInfo) { s.SplitDevs = nil }},
|
||||||
|
{name: "moteur Vulkan : refusé", cfg: tensor, why: "CUDA",
|
||||||
|
si: func(s *serveSysInfo) { s.SplitDevs = []splitDev{{"Vulkan0", 16311}, {"Vulkan1", 12288}} }},
|
||||||
|
{name: "--device d'une seule carte : refusé", cfg: tensor, extra: []string{"--device", "CUDA0"}, why: "une seule carte"},
|
||||||
|
{name: "--device inconnu : refusé", cfg: tensor, extra: []string{"-dev", "CUDA0,CUDA7"}, why: "CUDA7"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
si := tensorReady()
|
||||||
|
if c.si != nil {
|
||||||
|
c.si(&si)
|
||||||
|
}
|
||||||
|
on, why, _, _ := splitPlan(c.cfg, c.extra, si)
|
||||||
|
if on != c.on {
|
||||||
|
t.Fatalf("on = %v, attendu %v (%q)", on, c.on, why)
|
||||||
|
}
|
||||||
|
if !strings.Contains(why, c.why) || (c.why == "" && why != "") {
|
||||||
|
t.Errorf("raison %q, attendu %q", why, c.why)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitTensorArgs(t *testing.T) {
|
||||||
|
const bin = "/opt/llama/llama-server"
|
||||||
|
cfg := map[string]string{"SPLIT_MODE": "tensor", "CTX": "65536", "NGL": "999"}
|
||||||
|
|
||||||
|
// Clé absente : la ligne et l'environnement d'avant, à l'octet près, même
|
||||||
|
// sur un moteur et une machine où le mode tensor serait possible.
|
||||||
|
plain := map[string]string{"CTX": "65536", "NGL": "999"}
|
||||||
|
got, env, _ := buildServeArgs(plain, nil, bin, tensorReady())
|
||||||
|
si := tensorReady()
|
||||||
|
si.Help, si.SplitDevs = helpRecent, nil
|
||||||
|
want, wantEnv, _ := buildServeArgs(plain, nil, bin, si)
|
||||||
|
if !reflect.DeepEqual(got, want) || !reflect.DeepEqual(env, wantEnv) {
|
||||||
|
t.Fatalf("sans SPLIT_MODE : %q %v, attendu %q %v", got, env, want, wantEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, env, notes := buildServeArgs(cfg, nil, bin, tensorReady())
|
||||||
|
tail := []string{"-ngl", "all", "-sm", "tensor", "-ts", "57,43", "-fa", "on"}
|
||||||
|
if i := slices.Index(got, "-ngl"); i < 0 || !reflect.DeepEqual(got[i:i+len(tail)], tail) {
|
||||||
|
t.Fatalf("args = %q, attendu …%q…", got, tail)
|
||||||
|
}
|
||||||
|
if env["GGML_CUDA_ALLREDUCE"] != "internal" {
|
||||||
|
t.Errorf("GGML_CUDA_ALLREDUCE = %q, attendu internal (NCCL compresse en BF16)", env["GGML_CUDA_ALLREDUCE"])
|
||||||
|
}
|
||||||
|
if len(notes) == 0 || !strings.Contains(strings.Join(notes, "\n"), "NCCL") {
|
||||||
|
t.Errorf("la note doit parler de NCCL : %q", notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les choix de l'utilisateur gagnent et ne sont jamais doublés.
|
||||||
|
extra := []string{"-ts", "60,40", "-fa", "on", "-ngl", "all"}
|
||||||
|
got, _, _ = buildServeArgs(cfg, extra, bin, tensorReady())
|
||||||
|
for _, f := range []string{"-ts", "-fa", "-ngl", "-sm"} {
|
||||||
|
if n := countFlag(got, f); n != 1 {
|
||||||
|
t.Errorf("%s présent %d fois : %q", f, n, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --device dans l'ordre inverse : la répartition suit.
|
||||||
|
got, _, _ = buildServeArgs(cfg, []string{"--device", "CUDA1,CUDA0"}, bin, tensorReady())
|
||||||
|
if argValue(got, "-ts") != "43,57" {
|
||||||
|
t.Errorf("-ts = %q, attendu 43,57", argValue(got, "-ts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GGML_CUDA_ALLREDUCE déjà posé : gardé, et NCCL signalé comme avec perte.
|
||||||
|
si = tensorReady()
|
||||||
|
si.UserEnv["GGML_CUDA_ALLREDUCE"] = "nccl"
|
||||||
|
_, env, notes = buildServeArgs(cfg, nil, bin, si)
|
||||||
|
if _, set := env["GGML_CUDA_ALLREDUCE"]; set {
|
||||||
|
t.Errorf("choix de l'utilisateur écrasé : %v", env)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(notes, "\n"), "BF16") {
|
||||||
|
t.Errorf("NCCL doit être signalé : %q", notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les réglages qui lisent le placement voient le mode tensor : pas de
|
||||||
|
// graphes Q/K/V en parallèle, pas de --fit-target, pas de file CUDA.
|
||||||
|
withOpts := map[string]string{"CUDA_GRAPH_OPT": "on", "FIT_TARGET": "1024"}
|
||||||
|
for k, v := range cfg {
|
||||||
|
withOpts[k] = v
|
||||||
|
}
|
||||||
|
got, env, _ = buildServeArgs(withOpts, nil, bin, tensorReady())
|
||||||
|
if env["GGML_CUDA_GRAPH_OPT"] != "" || slices.Contains(got, "--fit-target") {
|
||||||
|
t.Errorf("mode tensor : ni GGML_CUDA_GRAPH_OPT ni --fit-target, %q %v", got, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moteur sans -sm tensor : aucun compagnon (-ngl all, -ts, -fa), une note.
|
||||||
|
si = tensorReady()
|
||||||
|
si.Help = helpRecent
|
||||||
|
got, env, notes = buildServeArgs(cfg, nil, bin, si)
|
||||||
|
if slices.Contains(got, "-sm") || slices.Contains(got, "-ts") || slices.Contains(got, "-fa") ||
|
||||||
|
argValue(got, "-ngl") != "auto" || env["GGML_CUDA_ALLREDUCE"] != "" {
|
||||||
|
t.Errorf("refusé : ligne d'avant attendue, %q %v", got, env)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(notes, "\n"), "SPLIT_MODE=tensor refusé") {
|
||||||
|
t.Errorf("le refus doit se dire : %q", notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countFlag(args []string, f string) int {
|
||||||
|
n := 0
|
||||||
|
for _, a := range args {
|
||||||
|
if a == f {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitTensorSlotPersist(t *testing.T) {
|
||||||
|
si := tensorReady()
|
||||||
|
si.Help += "--slot-save-path PATH\n"
|
||||||
|
si.SlotDir = "/data/slots"
|
||||||
|
cfg := map[string]string{"SLOT_PERSIST": "on", "HOST": "127.0.0.1", "SPLIT_MODE": "tensor"}
|
||||||
|
if ok, why := slotPersistPlan(cfg, nil, si); ok || !strings.Contains(why, "SPLIT_MODE") {
|
||||||
|
t.Errorf("SLOT_PERSIST avec SPLIT_MODE : refusé attendu, got %v %q", ok, why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpSupportsSplitTensor(t *testing.T) {
|
||||||
|
if helpSupportsSplitTensor(helpRecent + "-ts, --tensor-split N0,N1 fraction\n--override-tensor\n") {
|
||||||
|
t.Error("« tensor » ailleurs dans l'aide ne suffit pas")
|
||||||
|
}
|
||||||
|
if !helpSupportsSplitTensor(helpTensor) {
|
||||||
|
t.Error("liste littérale de -sm : oui")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,12 @@ var configTemplate = []struct{ key, help string }{
|
|||||||
"vide = 1024, minimum 1024 ; sans effet si NGL chiffré, --tensor-split, -ot ou --n-cpu-moe"},
|
"vide = 1024, minimum 1024 ; sans effet si NGL chiffré, --tensor-split, -ot ou --n-cpu-moe"},
|
||||||
{"OP_OFFLOAD_MIN_BATCH", "experts MoE sur CPU : taille de lot à partir de laquelle ils sont recopiés vers le GPU ; " +
|
{"OP_OFFLOAD_MIN_BATCH", "experts MoE sur CPU : taille de lot à partir de laquelle ils sont recopiés vers le GPU ; " +
|
||||||
"vide = défaut du moteur (32) ; plus haut = lots courts sur CPU, à mesurer"},
|
"vide = défaut du moteur (32) ; plus haut = lots courts sur CPU, à mesurer"},
|
||||||
|
{"SPLIT_MODE", "tensor = parallélisme de tenseurs entre cartes CUDA (-sm tensor, expérimental) : décodage plus rapide, " +
|
||||||
|
"prefill plus lent, à mesurer par tour. Loki pose -ngl all, -ts au prorata de la VRAM, -fa on (sauf réglés à la main) " +
|
||||||
|
"et GGML_CUDA_ALLREDUCE=internal (NCCL compresse en BF16, avec perte). Refusé sans la liste {none,layer,row,tensor} " +
|
||||||
|
"dans l'aide du moteur, avec un cache KV autre que f16/bf16/f32, des poids ou le KV sur CPU, --backend-sampling, " +
|
||||||
|
"-fa off, -sm à la main, SPEC, SIDE_SLOT, CTX non chiffré, une seule carte ou une VRAM estimée insuffisante. " +
|
||||||
|
"Vide/off (défaut) = découpe par couches"},
|
||||||
{"CUDA_GRAPH_OPT", "on = GGML_CUDA_GRAPH_OPT=1, branches Q/K/V en parallèle au décodage (expérimental, " +
|
{"CUDA_GRAPH_OPT", "on = GGML_CUDA_GRAPH_OPT=1, branches Q/K/V en parallèle au décodage (expérimental, " +
|
||||||
"sortie identique, gain de 0 à quelques %) ; off puis redémarrage s'il plante"},
|
"sortie identique, gain de 0 à quelques %) ; off puis redémarrage s'il plante"},
|
||||||
{"KV_TYPE", "quantization du cache KV ; vide = f16, sorties de référence ; q8_0 les modifie légèrement, " +
|
{"KV_TYPE", "quantization du cache KV ; vide = f16, sorties de référence ; q8_0 les modifie légèrement, " +
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ func modelLoadError() string {
|
|||||||
strings.Contains(low, "failed to initialize speculative decoding context"):
|
strings.Contains(low, "failed to initialize speculative decoding context"):
|
||||||
// Avant l'échec générique : c'est le brouillon qui manque, pas le modèle.
|
// Avant l'échec générique : c'est le brouillon qui manque, pas le modèle.
|
||||||
reason = "le décodage spéculatif n'a pas pu démarrer (tête MTP ou modèle brouillon) — SPEC=off dans le preset"
|
reason = "le décodage spéculatif n'a pas pu démarrer (tête MTP ou modèle brouillon) — SPEC=off dans le preset"
|
||||||
|
case strings.Contains(low, "split_mode_tensor requires flash_attn"),
|
||||||
|
strings.Contains(low, "split_mode_tensor not implemented"):
|
||||||
|
// Les autres lignes qui citent SPLIT_MODE_TENSOR ne sont que des avis.
|
||||||
|
reason = "le mode tensor (-sm tensor) n'a pas pu démarrer — SPLIT_MODE=off dans le preset"
|
||||||
case strings.Contains(l, "has offset") && strings.Contains(l, "expected"):
|
case strings.Contains(l, "has offset") && strings.Contains(l, "expected"):
|
||||||
reason = "format de quantification non reconnu par ce moteur"
|
reason = "format de quantification non reconnu par ce moteur"
|
||||||
case strings.Contains(low, "unknown model architecture"),
|
case strings.Contains(low, "unknown model architecture"),
|
||||||
|
|||||||
Reference in new issue
Block a user