Spéculation : SPEC=ngram et mtp+ngram, n-grammes du contexte en opt-in

En mode Code, le modèle recopie sans cesse ce qui est déjà dans le
contexte : chemins, diffs, arguments JSON, fichiers réécrits. ngram-mod
de llama.cpp y cherche la suite des 24 derniers jetons et propose d'un
coup les 48 à 64 suivants, sans modèle brouillon. La clé SPEC (off par
défaut, inchangé) gagne deux valeurs : ngram, et mtp+ngram qui y ajoute
la tête MTP. Même vérification exacte que MTP : distribution inchangée,
mais pas le même texte au bit près (vérification par lots), donc on
compare des sessions rejouées, pas des diffs.

- forme explicite seulement : --spec-type ngram-mod
  --spec-ngram-mod-n-match 24 --spec-ngram-mod-n-min 48
  --spec-ngram-mod-n-max 64 ; jamais --spec-default (TODO amont, contenu
  susceptible de changer avec le moteur)
- garde-fou d'aide sur « --spec-ngram-mod-n-match » (pas le simple mot
  ngram-mod, encore listé par les moteurs d'avant le renommage) ; absent =
  SPEC ignoré, note au journal
- mtp+ngram : --spec-type draft-mtp,ngram-mod seulement avec draft-mtp
  dans l'aide ET une tête MTP réelle (tenseur nextn ou MODEL_DRAFT) ;
  sinon n-grammes seuls, avec la raison — jamais « failed to create MTP
  context » en boucle
- EXTRA_ARGS/LLAMA_ARG_* : mêmes exclusions qu'avant, plus --spec_type et
  tout --spec-ngram-* (--spec-type s'additionne sans prévenir) ; pour
  ngram seul, un --spec-draft-* fait aussi taire Loki
- poids sur CPU (experts MoE, -ot, NGL partiel) : refusé tant que
  GGML_OP_OFFLOAD_MIN_BATCH (env ou OP_OFFLOAD_MIN_BATCH) ne dépasse pas
  le lot de vérification de 65 jetons, avec la suggestion
  OP_OFFLOAD_MIN_BATCH=128 ; avertissements pour un modèle plus gros que
  la RAM, les points de reprise d'un hybride, NGL imposé (logits)
- avec ngram, une tête MTP ou MODEL_DRAFT inutilisés sont signalés
- --spec-synth-len/--spec-synth-rates (et LLAMA_ARG_SPEC_SYNTH_LEN) :
  acceptation au hasard, avertissement au lancement ; Loki ne les pose
  jamais
- télémétrie : /api/perf/summary donne draft_n, draft_accepted et
  draft_rate par nature de requête
- UI : deux options du sélecteur de décodage spéculatif ; README et
  configTemplate
- tests : forme explicite, garde d'aide, repli de mtp+ngram, exclusions,
  seuil d'offload, ligne inchangée sans SPEC, acceptation par nature

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 10:28:25 +02:00
1 parent 28ba44eb46
commit 539e36ea49
11 files changed
+495 -67

No files matched your search

+30
View File
@@ -531,6 +531,36 @@ Ajoutées par ce fork :
exactement les jetons gardés (gabarit qui rend le dernier tour à
l'identique), sinon recalcul comme avant. Ce que voit le modèle ne change
pas : llama.cpp ne reprend un état que sur un préfixe de jetons identique.
- **Spéculation par n-grammes** (clé `SPEC`, valeurs `ngram` et `mtp+ngram`,
**off** par défaut, `loki config set SPEC ngram`, moteur relancé) : en mode
Code, le modèle recopie sans cesse ce qui est déjà dans le contexte (chemins,
diffs, arguments JSON, fichiers réécrits). `ngram` y cherche la suite des
24 derniers jetons et propose d'un coup les 48 à 64 suivants
(`--spec-type ngram-mod --spec-ngram-mod-n-match 24 --spec-ngram-mod-n-min 48
--spec-ngram-mod-n-max 64`, toujours en clair, jamais `--spec-default` dont
le contenu peut changer d'une version à l'autre) ; `mtp+ngram` ajoute la tête
MTP (`--spec-type draft-mtp,ngram-mod`), les n-grammes passant d'abord quand
ils trouvent. Chaque jeton reste tiré par le modèle et un jeton proposé n'est
gardé que s'il coïncide : même distribution, mais pas le même texte au bit
près (la vérification par lots n'emprunte pas les noyaux du décodage jeton
par jeton) — on compare des sessions rejouées, pas des diffs. Rien n'est posé
si le moteur ne connaît pas `--spec-ngram-mod-n-match`, si `EXTRA_ARGS` ou
`LLAMA_ARG_SPEC_TYPE` règlent déjà la spéculation (`--spec-type`, qui
s'additionne au lieu de remplacer, `--spec-default`, `--spec-ngram-*`, `-md`…),
ou, pour `ngram`, si un `--spec-draft-*` y figure. `mtp+ngram` sans tête MTP
(ni dans le fichier ni en `MODEL_DRAFT`) ou sans `draft-mtp` dans le moteur
retombe sur les n-grammes seuls, avec une note. Avec `ngram`, une tête MTP
présente reste inutilisée (le type explicite coupe le choix automatique) : la
note le dit. Poids sur CPU (`--n-cpu-moe`, `-ot …=CPU`, `NGL` partiel) :
refusé tant que `GGML_OP_OFFLOAD_MIN_BATCH` (clé `OP_OFFLOAD_MIN_BATCH`) ne
dépasse pas le lot de vérification de 65 jetons — sinon chaque brouillon
recopierait les experts vers le GPU ; `OP_OFFLOAD_MIN_BATCH=128` pour
l'essayer, en mesurant aussi les lectures disque si le modèle dépasse la RAM.
Sur un hybride, chaque brouillon copie l'état récurrent (point de reprise)
et le recharge s'il est rejeté. `/api/perf/summary` donne l'acceptation par
nature de requête (`kinds.<nature>.draft_rate`) : à garder là où elle paie.
Les drapeaux d'acceptation synthétique (`--spec-synth-*`, « benchmarking
only »), que Loki ne pose jamais, déclenchent un avertissement au lancement.
- **Discussions multiples** : historique complet dans la barre latérale, titre
repris du premier message (renommable), suppression. **Chaque discussion a son
dossier de fichiers** (`workspace/discussions/<id>/`) : les pièces jointes
+1 -1
View File
@@ -347,7 +347,7 @@ func cmdServe(args []string) error {
probeCkptEnv(&si)
probeSideSlotEnv(cfg, &si)
spec := specMode(cfg)
if spec == "auto" || spec == "mtp" {
if spec != "off" && spec != "" {
probeSpec(cfg, &si)
}
// Build du moteur : seulement pour un hybride ou SPEC=auto, les seuls cas où
+11 -1
View File
@@ -23,7 +23,7 @@ import (
// ils décident. LLAMA_ARG_NO_CONTEXT_SHIFT est celle des moteurs anciens.
var fidelityArgEnv = []string{"LLAMA_ARG_CACHE_TYPE_K", "LLAMA_ARG_CACHE_TYPE_V",
"LLAMA_ARG_CONTEXT_SHIFT", "LLAMA_ARG_NO_CONTEXT_SHIFT", "LLAMA_ARG_CACHE_REUSE",
"LLAMA_ARG_MMPROJ", "LLAMA_ARG_MMPROJ_URL"}
"LLAMA_ARG_MMPROJ", "LLAMA_ARG_MMPROJ_URL", "LLAMA_ARG_SPEC_SYNTH_LEN"}
// probeFidelityEnv complète ArgEnv avec fidelityArgEnv. Appelée après
// probeServeGPUs, qui crée ArgEnv.
@@ -168,6 +168,16 @@ func lossyCacheNotes(extra []string, si serveSysInfo) []string {
"de cache calculés sous un autre préfixe : les sorties ne sont plus exactement celles d'un calcul "+
"complet%s.", n, src, ignored))
}
// Acceptation SYNTHÉTIQUE (« benchmarking only ») : le moteur garde des
// jetons du brouillon au hasard, sans les comparer à ce qu'aurait tiré le
// modèle. Loki ne les pose jamais ; écrits à la main, on le crie.
if f := hasFlagPrefix(extra, "--spec-synth-"); f != "" || si.ArgEnv["LLAMA_ARG_SPEC_SYNTH_LEN"] != "" {
if f == "" {
f = "LLAMA_ARG_SPEC_SYNTH_LEN"
}
notes = append(notes, "avertissement : "+f+" — le moteur accepte des jetons du brouillon AU HASARD, sans "+
"vérification : les réponses ne sont plus celles du modèle. Réservé aux mesures, retire-le.")
}
return notes
}
+238 -48
View File
@@ -22,6 +22,8 @@ import (
// MODEL_DRAFT=<nom> → la tête ou le brouillon, résolu comme MMPROJ
// SPEC_N_MAX=<n> → --spec-draft-n-max (défaut du moteur : 3)
// SPEC_SAMPLING=probabilistic → seulement avec SPEC=mtp ; greedy sinon
// SPEC=ngram → n-grammes tirés du contexte (ngram-mod), sans brouillon
// SPEC=mtp+ngram → les deux ; un n-gramme trouvé passe avant la tête MTP
//
// Rien n'y touche au modèle : chaque jeton émis est tiré par l'échantillonneur
// du modèle cible, un jeton du brouillon n'est gardé que s'il coïncide
@@ -30,6 +32,11 @@ import (
// empêcher le moteur de démarrer. D'où l'opt-in, les garde-fous d'auto et le
// jeton de tentative (specAutoVerdict) : un lancement automatique qui n'a
// jamais répondu ne se retente pas avec la même configuration.
//
// Une vérification par lot n'emprunte pas les mêmes noyaux qu'un décodage jeton
// par jeton : même distribution, mais pas le même texte au bit près — deux
// candidats presque à égalité peuvent s'inverser à graine égale, comme entre
// prefill et décodage. On compare donc des sessions rejouées, pas des diffs.
const (
// specAutoMinBuild : premier build officiel où Loki ose l'auto (MTP serveur,
@@ -37,6 +44,19 @@ const (
// peut connaître le drapeau sans le tenir.
specAutoMinBuild = 11009
specNMaxLimit = 64
// Bornes de ngram-mod, toujours écrites en clair : --spec-default pose les
// mêmes aujourd'hui, mais son code porte un TODO qui annonce d'autres types —
// une mise à jour du moteur changerait le réglage sans un mot. Un brouillon
// part dès 24 jetons qui se répètent mot pour mot, et fait 48 à 64 jetons :
// ce que recopie une boucle d'outils (chemins, diffs, arguments JSON).
specNgramNMatch = 24
specNgramNMin = 48
specNgramNMax = 64
// ggmlOffloadMinBatchDefault : seuil par défaut du moteur (ggml-cuda.cu) à
// partir duquel un lot recopie vers le GPU les poids restés en RAM.
ggmlOffloadMinBatchDefault = 32
)
// specArgEnv : les variables qui règlent la spéculation sans drapeau. Posées,
@@ -45,10 +65,28 @@ var specArgEnv = []string{"LLAMA_ARG_SPEC_TYPE", "LLAMA_ARG_SPEC_DRAFT_MODEL", "
"LLAMA_ARG_SPEC_DRAFT_N_MAX", "LLAMA_ARG_SPEC_DRAFT_SAMPLING"}
// specUserFlags : la spéculation réglée à la main. Les ajouter EN PLUS ferait
// cumuler les types (--spec-type s'additionne) ou charger deux brouillons.
var specUserFlags = []string{"--spec-type", "-md", "--model-draft", "--spec-draft-model",
// cumuler les types (--spec-type s'additionne, sans l'avertissement « specified
// multiple times » des autres drapeaux) ou charger deux brouillons.
var specUserFlags = []string{"--spec-type", "--spec_type", "-md", "--model-draft", "--spec-draft-model",
"-hfd", "-hfrd", "--spec-draft-hf", "--hf-repo-draft", "--spec-default"}
// specUserPrefixes : des réglages de n-grammes à la main (--spec-ngram-mod-n-max,
// --spec-ngram-simple-size-n…) disent la même chose qu'un --spec-type.
var specUserPrefixes = []string{"--spec-ngram-", "--spec_ngram_"}
// hasFlagPrefix : un drapeau d'args commence-t-il par l'un de ces préfixes ?
func hasFlagPrefix(args []string, prefixes ...string) string {
for _, a := range args {
for _, p := range prefixes {
if strings.HasPrefix(a, p) {
name, _, _ := strings.Cut(a, "=")
return name
}
}
}
return ""
}
// probeSpec : variables du moteur, puis MODEL_DRAFT résolu comme MMPROJ (nom
// simple cherché dans les dossiers déclarés, ou chemin absolu). Introuvable ne
// bloque PAS le lancement, contrairement au projecteur : le brouillon n'est
@@ -91,20 +129,23 @@ func specSidecarArch(arch string) bool {
return false
}
// specMode lit SPEC : "off", "auto", "mtp", ou "" si illisible.
// specMode lit SPEC : "off", "auto", "mtp", "ngram", "mtp+ngram", ou "" si
// illisible.
func specMode(cfg map[string]string) string {
switch v := strings.ToLower(strings.TrimSpace(cfg["SPEC"])); v {
case "", "off", "non", "no", "0":
return "off"
case "auto", "mtp":
case "auto", "mtp", "ngram", "mtp+ngram":
return v
case "ngram+mtp":
return "mtp+ngram"
}
return ""
}
// specUserSet : EXTRA_ARGS ou l'environnement règlent déjà la spéculation.
func specUserSet(extra []string, argEnv map[string]string) bool {
if hasAnyFlag(extra, specUserFlags...) {
if hasAnyFlag(extra, specUserFlags...) || hasFlagPrefix(extra, specUserPrefixes...) != "" {
return true
}
for _, k := range []string{"LLAMA_ARG_SPEC_TYPE", "LLAMA_ARG_SPEC_DRAFT_MODEL", "LLAMA_ARG_SPEC_DRAFT_HF_REPO"} {
@@ -204,7 +245,7 @@ func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
draftKey := strings.TrimSpace(cfg["MODEL_DRAFT"])
switch mode {
case "":
return nil, []string{"SPEC=" + cfg["SPEC"] + " illisible (off, auto ou mtp) : sans décodage spéculatif"}, false
return nil, []string{"SPEC=" + cfg["SPEC"] + " illisible (off, auto, mtp, ngram ou mtp+ngram) : sans décodage spéculatif"}, false
case "off":
if draftKey != "" {
return nil, []string{"MODEL_DRAFT ignoré : SPEC=off (auto ou mtp pour s'en servir)"}, false
@@ -218,49 +259,40 @@ func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
return nil, []string{"SPEC=" + mode + " : " + why + " — sans décodage spéculatif"}, false
}
// La source : MODEL_DRAFT s'il est posé, sinon la tête MTP du modèle. Dans
// les deux cas, on regarde les TENSEURS, comme llama.cpp : une clé
// nextn_predict_layers seule ne prouve pas que la tête est dans le fichier.
mtp := false
switch {
case draftKey != "":
switch {
case si.Draft == "":
why := "MODEL_DRAFT=" + draftKey + " introuvable"
if si.DraftErr != "" {
why += " (" + si.DraftErr + ")"
}
// N-grammes : leurs garde-fous d'abord, ils valent pour les deux formes.
if mode == "ngram" || mode == "mtp+ngram" {
if why := ngramBlocker(cfg, extra, si, mode); why != "" {
return skip(why)
case si.DraftGGUF == nil:
return skip("MODEL_DRAFT illisible (GGUF incomplet ou en cours de téléchargement ?)")
case si.DraftGGUF.HasNextNTensor:
if !helpSupportsMTP(si.Help) {
return skip("ce moteur ne connaît pas draft-mtp")
}
// Type explicite : llama.cpp ne le devine que sur la première tranche.
args, mtp = []string{"-md", si.Draft, "--spec-type", "draft-mtp"}, true
case specSidecarArch(si.DraftGGUF.Arch):
return skip("MODEL_DRAFT est une tête " + si.DraftGGUF.Arch +
", pas un modèle brouillon : règle -md et --spec-type dans EXTRA_ARGS")
default:
if !strings.Contains(si.Help, "--spec-type") || !strings.Contains(si.Help, "draft-simple") {
return skip("ce moteur ne connaît pas --spec-type draft-simple")
}
// Sans type, un brouillon qui n'est pas une tête MTP serait chargé en
// VRAM puis jamais utilisé.
args = []string{"-md", si.Draft, "--spec-type", "draft-simple"}
}
case si.GGUF == nil:
return skip("métadonnées du modèle illisibles")
case !si.GGUF.HasNextNTensor:
if mode == "mtp" || si.GGUF.NextN > 0 {
return skip("pas de tête MTP dans ce fichier (publiée à part ? MODEL_DRAFT=mtp-….gguf)")
}
if mode == "ngram" {
args, notes = ngramSpecArgs(cfg, extra, si, "")
return args, notes, false
}
src, mtp, why, quiet := specSource(mode, draftKey, si)
if mode == "mtp+ngram" && !mtp {
// Pas de tête MTP utilisable : la passer quand même ferait mourir le
// moteur sur « failed to create MTP context », et boucler. Les n-grammes,
// eux, n'ont besoin de rien.
if why == "" {
why = "MODEL_DRAFT n'est pas une tête MTP"
}
return nil, nil, false // auto sur un modèle sans MTP : rien à dire
case !helpSupportsMTP(si.Help):
return skip("ce moteur ne connaît pas draft-mtp")
default:
args, mtp = []string{"--spec-type", "draft-mtp"}, true
args, notes = ngramSpecArgs(cfg, extra, si, "SPEC=mtp+ngram : sans MTP ("+why+") → n-grammes seuls. ")
return args, notes, false
}
if why != "" {
return skip(why)
}
if quiet {
return nil, nil, false
}
args = src
if mode == "mtp+ngram" {
// Une seule liste : --spec-type s'additionne, mais une valeur unique se
// lit d'un coup d'œil dans le journal.
args[len(args)-1] = "draft-mtp,ngram-mod"
args = append(args, ngramModParams()...)
}
label := "brouillon " + baseName(si.Draft)
@@ -275,7 +307,10 @@ func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
notes = append(notes, "SPEC=auto → "+label+" : sortie inchangée (chaque jeton est vérifié par le modèle), "+
"~1-2 Go de VRAM en plus. Mesure le prefill ; SPEC=off pour couper.")
} else {
note := "SPEC=mtp → " + label + " imposé : sortie inchangée, ~1-2 Go de VRAM en plus"
note := "SPEC=" + mode + " → " + label + " imposé : sortie inchangée, ~1-2 Go de VRAM en plus"
if mode == "mtp+ngram" {
note += " ; n-grammes en plus (ngram-mod, prioritaires quand ils trouvent une répétition)"
}
if why := fitBlocker(cfg, extra, si); why != "" || hasAnyFlag(extra, "-dev", "--device") {
if why == "" {
why = "--device"
@@ -290,6 +325,9 @@ func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
note += " ; MTP qwen4exp très récent"
}
notes = append(notes, note)
if mode == "mtp+ngram" {
notes = append(notes, ngramCostNotes(cfg, extra, si)...)
}
}
// Nombre de jetons anticipés : défaut du moteur (3) si la clé est vide.
@@ -314,7 +352,7 @@ func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
case "", "greedy":
case "probabilistic":
switch {
case mode != "mtp":
case mode == "auto":
notes = append(notes, "SPEC_SAMPLING=probabilistic ignoré avec SPEC=auto (SPEC=mtp pour le choisir) : greedy")
case statefulSampler(extra) != "":
notes = append(notes, "SPEC_SAMPLING=probabilistic refusé avec "+statefulSampler(extra)+
@@ -334,6 +372,158 @@ func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, not
return args, notes, auto
}
// specSource : d'où vient le brouillon de SPEC=auto|mtp|mtp+ngram. MODEL_DRAFT
// s'il est posé, sinon la tête MTP du modèle ; dans les deux cas on regarde les
// TENSEURS, comme llama.cpp : une clé nextn_predict_layers seule ne prouve pas
// que la tête est dans le fichier. why non vide = aucun brouillon ; quiet =
// rien à dire (auto sur un modèle sans MTP).
func specSource(mode, draftKey string, si serveSysInfo) (args []string, mtp bool, why string, quiet bool) {
switch {
case draftKey != "":
switch {
case si.Draft == "":
why := "MODEL_DRAFT=" + draftKey + " introuvable"
if si.DraftErr != "" {
why += " (" + si.DraftErr + ")"
}
return nil, false, why, false
case si.DraftGGUF == nil:
return nil, false, "MODEL_DRAFT illisible (GGUF incomplet ou en cours de téléchargement ?)", false
case si.DraftGGUF.HasNextNTensor:
if !helpSupportsMTP(si.Help) {
return nil, false, "ce moteur ne connaît pas draft-mtp", false
}
// Type explicite : llama.cpp ne le devine que sur la première tranche.
return []string{"-md", si.Draft, "--spec-type", "draft-mtp"}, true, "", false
case specSidecarArch(si.DraftGGUF.Arch):
return nil, false, "MODEL_DRAFT est une tête " + si.DraftGGUF.Arch +
", pas un modèle brouillon : règle -md et --spec-type dans EXTRA_ARGS", false
case mode == "mtp+ngram":
return nil, false, "", false // un modèle brouillon n'est pas une tête MTP
default:
if !strings.Contains(si.Help, "--spec-type") || !strings.Contains(si.Help, "draft-simple") {
return nil, false, "ce moteur ne connaît pas --spec-type draft-simple", false
}
// Sans type, un brouillon qui n'est pas une tête MTP serait chargé en
// VRAM puis jamais utilisé.
return []string{"-md", si.Draft, "--spec-type", "draft-simple"}, false, "", false
}
case si.GGUF == nil:
return nil, false, "métadonnées du modèle illisibles", false
case !si.GGUF.HasNextNTensor:
if mode != "auto" || si.GGUF.NextN > 0 {
return nil, false, "pas de tête MTP dans ce fichier (publiée à part ? MODEL_DRAFT=mtp-….gguf)", false
}
return nil, false, "", true
case !helpSupportsMTP(si.Help):
return nil, false, "ce moteur ne connaît pas draft-mtp", false
}
return []string{"--spec-type", "draft-mtp"}, true, "", false
}
// ngramModParams : les trois bornes de ngram-mod, toujours explicites.
func ngramModParams() []string {
return []string{
"--spec-ngram-mod-n-match", strconv.Itoa(specNgramNMatch),
"--spec-ngram-mod-n-min", strconv.Itoa(specNgramNMin),
"--spec-ngram-mod-n-max", strconv.Itoa(specNgramNMax),
}
}
// ngramBlocker dit pourquoi SPEC=ngram|mtp+ngram ne s'active pas, ou vide.
//
// - Le moteur doit connaître --spec-ngram-mod-n-match, pas seulement le mot
// « ngram-mod » : les moteurs d'avant le renommage ont encore le type dans
// leur liste mais refusent les drapeaux actuels, et meurent au démarrage.
// - SPEC=ngram n'a pas de brouillon : un --spec-draft-* d'EXTRA_ARGS veut dire
// qu'on règle un brouillon à la main, on ne mélange pas.
// - Poids en RAM (experts MoE sur CPU…) : llama.cpp recopie vers le GPU les
// poids d'un lot dès GGML_OP_OFFLOAD_MIN_BATCH jetons (32 par défaut). Un
// lot de vérification de n_max+1 jetons au-dessus du seuil ferait passer
// les experts de chaque couche par le PCIe — voire par le disque si le
// modèle mappé dépasse la RAM — à chaque brouillon. Refusé tant que le seuil
// n'est pas relevé au-dessus du lot.
func ngramBlocker(cfg map[string]string, extra []string, si serveSysInfo, mode string) string {
if !strings.Contains(si.Help, "--spec-ngram-mod-n-match") {
return "ce moteur ne connaît pas --spec-ngram-mod-n-match"
}
if mode == "ngram" {
if f := hasFlagPrefix(extra, "--spec-draft-", "--spec_draft_"); f != "" {
return f + " dans EXTRA_ARGS (brouillon réglé à la main)"
}
}
blocks := 0
if si.GGUF != nil {
blocks = si.GGUF.BlockCount
}
if r := cpuWeights(cfg, extra, si.ArgEnv, blocks); r != "" {
if thr := opOffloadThreshold(cfg, si); specNgramNMax+1 >= thr {
return fmt.Sprintf("%s : un lot de vérification de %d jetons atteint GGML_OP_OFFLOAD_MIN_BATCH (%d) et "+
"recopierait ces poids vers le GPU à chaque brouillon — OP_OFFLOAD_MIN_BATCH=128 pour l'essayer, à mesurer",
r, specNgramNMax+1, thr)
}
}
return ""
}
// opOffloadThreshold : le seuil GGML_OP_OFFLOAD_MIN_BATCH que verra le moteur —
// celui de l'environnement, sinon celui d'OP_OFFLOAD_MIN_BATCH, sinon le défaut.
func opOffloadThreshold(cfg map[string]string, si serveSysInfo) int {
v := strings.TrimSpace(si.UserEnv["GGML_OP_OFFLOAD_MIN_BATCH"])
if v == "" {
v, _ = opOffloadMinBatchEnv(cfg, si)
}
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
return ggmlOffloadMinBatchDefault
}
// ngramSpecArgs : SPEC=ngram, ou le repli de mtp+ngram sans tête MTP (lead dit
// pourquoi). Pas de --spec-draft-* : il n'y a pas de brouillon.
func ngramSpecArgs(cfg map[string]string, extra []string, si serveSysInfo, lead string) (args, notes []string) {
args = append([]string{"--spec-type", "ngram-mod"}, ngramModParams()...)
notes = append(notes, lead+fmt.Sprintf("SPEC=ngram → n-grammes du contexte (ngram-mod, brouillons de %d à %d jetons) : "+
"distribution inchangée (chaque jeton vérifié), ~16 Mo de RAM ; utile surtout en mode code, à mesurer "+
"(acceptation par nature dans /api/perf/summary)", specNgramNMin, specNgramNMax))
if lead == "" {
// --spec-type explicite : llama.cpp ne choisit plus de lui-même la tête MTP.
switch {
case strings.TrimSpace(cfg["MODEL_DRAFT"]) != "":
notes = append(notes, "MODEL_DRAFT ignoré avec SPEC=ngram : SPEC=mtp+ngram pour les deux")
case si.GGUF != nil && si.GGUF.HasNextNTensor:
notes = append(notes, "tête MTP du modèle inutilisée avec SPEC=ngram : SPEC=mtp+ngram pour les deux")
}
}
if strings.TrimSpace(cfg["SPEC_N_MAX"]) != "" || strings.TrimSpace(cfg["SPEC_SAMPLING"]) != "" {
notes = append(notes, "SPEC_N_MAX et SPEC_SAMPLING ignorés : les n-grammes n'ont pas de brouillon à régler")
}
return args, append(notes, ngramCostNotes(cfg, extra, si)...)
}
// ngramCostNotes : ce que les longs brouillons coûtent, là où ça se voit.
func ngramCostNotes(cfg map[string]string, extra []string, si serveSysInfo) []string {
var notes []string
if si.GGUF != nil && si.GGUF.Hybrid {
notes = append(notes, "modèle hybride : chaque brouillon n-gramme copie l'état récurrent (point de reprise en RAM "+
"hôte) et le recharge s'il est rejeté — compare le temps par tour, pas seulement le débit")
}
blocks := 0
if si.GGUF != nil {
blocks = si.GGUF.BlockCount
}
if cpuWeights(cfg, extra, si.ArgEnv, blocks) != "" && si.ModelBytes > 0 && si.RAMMiB > 0 &&
si.ModelBytes > si.RAMMiB<<20 {
notes = append(notes, "modèle plus gros que la RAM, poids sur CPU : un lot de vérification lit la plupart des "+
"experts de chaque couche — surveille les lectures disque, pas seulement les jetons/s")
}
if nglForced(cfg, extra) {
notes = append(notes, fmt.Sprintf("NGL imposé (--fit inactif) : les logits de %d positions par slot prennent "+
"quelques dizaines de Mio de VRAM en plus", specNgramNMax+1))
}
return notes
}
// --- Jeton de tentative ------------------------------------------------------
//
// Un essai automatique qui fait tomber le moteur ne doit pas se rejouer : sous
+161
View File
@@ -332,3 +332,164 @@ func TestLastOffload(t *testing.T) {
t.Errorf("tout sur GPU : got %d/%d", n, m)
}
}
// helpNgram : un moteur qui connaît les drapeaux actuels de ngram-mod.
const helpNgram = helpSpec + `--spec-ngram-mod-n-min N minimum number of ngram tokens (default: 48)
--spec-ngram-mod-n-max N maximum number of ngram tokens (default: 64)
--spec-ngram-mod-n-match N ngram-mod lookup length (default: 24)
`
func TestSpecArgsNgram(t *testing.T) {
ngram := []string{"--spec-type", "ngram-mod",
"--spec-ngram-mod-n-match", "24", "--spec-ngram-mod-n-min", "48", "--spec-ngram-mod-n-max", "64"}
both := []string{"--spec-type", "draft-mtp,ngram-mod",
"--spec-ngram-mod-n-match", "24", "--spec-ngram-mod-n-min", "48", "--spec-ngram-mod-n-max", "64",
"--spec-draft-sampling", "greedy"}
cfgOf := func(kv ...string) map[string]string {
m := map[string]string{}
for i := 0; i+1 < len(kv); i += 2 {
m[kv[i]] = kv[i+1]
}
return m
}
dense := func(s *serveSysInfo) { s.GGUF = &GGUFInfo{Arch: "llama", BlockCount: 32} }
cases := []struct {
name string
cfg map[string]string
extra []string
si func(*serveSysInfo)
want []string
notes int
}{
{name: "ngram, modèle dense : forme explicite, jamais --spec-default",
cfg: cfgOf("SPEC", "ngram"), si: dense, want: ngram, notes: 1},
{name: "ngram+mtp s'écrit aussi dans l'autre sens",
cfg: cfgOf("SPEC", "ngram+mtp"), want: both, notes: 2},
{name: "ngram sur un modèle hybride à tête MTP : tête inutilisée et points de reprise dits",
cfg: cfgOf("SPEC", "ngram"), want: ngram, notes: 3},
{name: "ngram, moteur qui n'a que le type dans sa liste : rien",
cfg: cfgOf("SPEC", "ngram"), notes: 1, si: func(s *serveSysInfo) { s.Help = helpSpec }},
{name: "mtp+ngram, moteur sans drapeaux ngram-mod : rien",
cfg: cfgOf("SPEC", "mtp+ngram"), notes: 1, si: func(s *serveSysInfo) { s.Help = helpSpec }},
{name: "mtp+ngram avec la tête dans le fichier : une seule liste de types",
cfg: cfgOf("SPEC", "mtp+ngram"), want: both, notes: 2},
{name: "mtp+ngram sans tête MTP : n-grammes seuls, jamais « failed to create MTP context »",
cfg: cfgOf("SPEC", "mtp+ngram"), si: dense, want: ngram, notes: 1},
{name: "mtp+ngram, moteur sans draft-mtp : n-grammes seuls",
cfg: cfgOf("SPEC", "mtp+ngram"), want: ngram, notes: 2,
si: func(s *serveSysInfo) {
s.Help = strings.ReplaceAll(s.Help, "draft-mtp", "draft-xxx")
}},
{name: "mtp+ngram avec un petit modèle brouillon : n-grammes seuls, pas de -md",
cfg: cfgOf("SPEC", "mtp+ngram", "MODEL_DRAFT", "Qwen3-0.6B.gguf"), want: ngram, notes: 2,
si: func(s *serveSysInfo) {
s.Draft = "/models/Qwen3-0.6B.gguf"
s.DraftGGUF = &GGUFInfo{Arch: "qwen3", BlockCount: 28}
}},
{name: "mtp+ngram avec la tête MTP publiée à part",
cfg: cfgOf("SPEC", "mtp+ngram", "MODEL_DRAFT", "mtp-q.gguf"), notes: 2,
want: append([]string{"-md", "/models/mtp-q.gguf"}, both...),
si: func(s *serveSysInfo) {
s.GGUF.HasNextNTensor = false
s.Draft = "/models/mtp-q.gguf"
s.DraftGGUF = &GGUFInfo{Arch: "qwen35", BlockCount: 65, HasNextNTensor: true}
}},
{name: "ngram + MODEL_DRAFT : brouillon ignoré, et dit",
cfg: cfgOf("SPEC", "ngram", "MODEL_DRAFT", "mtp-q.gguf"), si: dense, want: ngram, notes: 2},
{name: "ngram + SPEC_N_MAX : ignoré, et dit",
cfg: cfgOf("SPEC", "ngram", "SPEC_N_MAX", "4"), si: dense, want: ngram, notes: 2},
{name: "--spec-type dans EXTRA_ARGS : Loki se tait",
cfg: cfgOf("SPEC", "ngram"), extra: []string{"--spec-type", "ngram-simple"}, notes: 1},
{name: "--spec_type=… dans EXTRA_ARGS : Loki se tait",
cfg: cfgOf("SPEC", "ngram"), extra: []string{"--spec_type=ngram-mod"}, notes: 1},
{name: "--spec-default dans EXTRA_ARGS : Loki se tait",
cfg: cfgOf("SPEC", "mtp+ngram"), extra: []string{"--spec-default"}, notes: 1},
{name: "--spec-ngram-mod-n-max dans EXTRA_ARGS : Loki se tait",
cfg: cfgOf("SPEC", "ngram"), extra: []string{"--spec-ngram-mod-n-max=16"}, notes: 1},
{name: "-md dans EXTRA_ARGS : Loki se tait",
cfg: cfgOf("SPEC", "mtp+ngram"), extra: []string{"-md", "d.gguf"}, notes: 1},
{name: "LLAMA_ARG_SPEC_TYPE posée : Loki se tait", cfg: cfgOf("SPEC", "ngram"), notes: 1,
si: func(s *serveSysInfo) { s.ArgEnv["LLAMA_ARG_SPEC_TYPE"] = "ngram-mod" }},
{name: "ngram + --spec-draft-n-max à la main : rien",
cfg: cfgOf("SPEC", "ngram"), si: dense, extra: []string{"--spec-draft-n-max", "8"}, notes: 1},
{name: "mtp+ngram + --spec-draft-n-max à la main : la règle de SPEC=mtp",
cfg: cfgOf("SPEC", "mtp+ngram"), extra: []string{"--spec-draft-n-max", "8"}, want: both, notes: 2},
{name: "experts sur CPU, seuil par défaut : refusé, OP_OFFLOAD_MIN_BATCH suggéré",
cfg: cfgOf("SPEC", "ngram"), si: dense, extra: []string{"--n-cpu-moe", "40"}, notes: 1},
{name: "experts sur CPU, mtp+ngram : refusé aussi",
cfg: cfgOf("SPEC", "mtp+ngram"), extra: []string{"-ot", "exps=CPU"}, notes: 1},
{name: "experts sur CPU, OP_OFFLOAD_MIN_BATCH=64 : encore sous le lot de 65, refusé",
cfg: cfgOf("SPEC", "ngram", "OP_OFFLOAD_MIN_BATCH", "64"), si: dense, extra: []string{"-cmoe"}, notes: 1},
{name: "experts sur CPU, OP_OFFLOAD_MIN_BATCH=128 : accepté",
cfg: cfgOf("SPEC", "ngram", "OP_OFFLOAD_MIN_BATCH", "128"), si: dense, extra: []string{"-cmoe"}, want: ngram, notes: 1},
{name: "experts sur CPU, GGML_OP_OFFLOAD_MIN_BATCH=256 dans l'environnement : accepté",
cfg: cfgOf("SPEC", "ngram"), extra: []string{"--n-cpu-moe", "40"}, want: ngram, notes: 1,
si: func(s *serveSysInfo) {
dense(s)
s.UserEnv = map[string]string{"GGML_OP_OFFLOAD_MIN_BATCH": "256"}
}},
{name: "experts sur CPU, modèle plus gros que la RAM : accepté avec l'avertissement",
cfg: cfgOf("SPEC", "ngram", "OP_OFFLOAD_MIN_BATCH", "128"), extra: []string{"--n-cpu-moe", "40"},
want: ngram, notes: 2,
si: func(s *serveSysInfo) {
dense(s)
s.ModelBytes, s.RAMMiB = 82<<30, 64<<10
}},
{name: "NGL imposé : logits en VRAM dits",
cfg: cfgOf("SPEC", "ngram", "NGL", "40"), si: dense, want: ngram, notes: 2},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
si := mtpReady()
si.Help = helpNgram
if c.si != nil {
c.si(&si)
}
got, notes, isAuto := specArgs(c.cfg, c.extra, si)
if !reflect.DeepEqual(got, c.want) {
t.Errorf("args = %q, attendu %q", got, c.want)
}
if isAuto {
t.Error("ngram n'est jamais automatique")
}
if len(notes) != c.notes {
t.Errorf("%d notes, attendu %d : %q", len(notes), c.notes, notes)
}
for _, a := range got {
if a == "--spec-default" || strings.HasPrefix(a, "--spec-synth") {
t.Errorf("drapeau interdit %q", a)
}
}
})
}
}
// Sans SPEC, la même ligne qu'avant, quel que soit le moteur.
func TestBuildServeArgsNgramDefault(t *testing.T) {
si := mtpReady()
si.Help = helpNgram
base, _, _ := buildServeArgs(map[string]string{}, nil, "/bin/llama-server", si)
for _, a := range base {
if strings.HasPrefix(a, "--spec") {
t.Fatalf("SPEC absent : aucun drapeau de spéculation, %q", base)
}
}
got, _, _ := buildServeArgs(map[string]string{"SPEC": "ngram"}, []string{"--jinja"}, "/bin/llama-server", si)
i := slices.Index(got, "--spec-type")
if i < 0 || got[i+1] != "ngram-mod" || slices.Index(got, "--jinja") < i {
t.Fatalf("SPEC=ngram : %q", got)
}
}
func TestSynthAcceptanceWarned(t *testing.T) {
for _, extra := range [][]string{{"--spec-synth-len", "3"}, {"--spec-synth-rates=0.5,0.5"}} {
if n := lossyCacheNotes(extra, serveSysInfo{ArgEnv: map[string]string{}}); len(n) != 1 ||
!strings.Contains(n[0], "AU HASARD") {
t.Errorf("%q : %q", extra, n)
}
}
si := serveSysInfo{ArgEnv: map[string]string{"LLAMA_ARG_SPEC_SYNTH_LEN": "3"}}
if n := lossyCacheNotes(nil, si); len(n) != 1 {
t.Errorf("variable : %q", n)
}
}
+12
View File
@@ -453,6 +453,12 @@ type perfKindSum struct {
LostEvents int `json:"lost_events"`
LostTokens int `json:"lost_tokens"`
TTFTMedianMs *float64 `json:"ttft_median_ms,omitempty"`
// Acceptation du brouillon par nature : un n-gramme paie en mode code
// (chemins, diffs recopiés) et presque pas en prose — le taux global
// mélangerait les deux. Absent = moteur muet ou spéculation coupée.
DraftN int `json:"draft_n,omitempty"`
DraftAccepted int `json:"draft_accepted,omitempty"`
DraftRate *float64 `json:"draft_rate,omitempty"`
}
type perfDepthSum struct {
@@ -550,8 +556,10 @@ func perfSummarize(recs []perfRec) perfSummary {
}
if r.DraftN != nil {
s.DraftN += *r.DraftN
k.DraftN += *r.DraftN
if r.DraftAcc != nil {
s.DraftAccepted += *r.DraftAcc
k.DraftAccepted += *r.DraftAcc
}
}
}
@@ -561,6 +569,10 @@ func perfSummarize(recs []perfRec) perfSummary {
k.PrefillSecPerTurn = k.PrefillSec / float64(k.Turns)
}
k.TTFTMedianMs = perfMedian(ttft[kind])
if k.DraftN > 0 {
r := float64(k.DraftAccepted) / float64(k.DraftN)
k.DraftRate = &r
}
}
if sumTotal > 0 {
r := float64(sumCached) / float64(sumTotal)
+7
View File
@@ -249,6 +249,13 @@ func TestPerfSummarize(t *testing.T) {
if s.DraftRate == nil || *s.DraftRate != 0.75 {
t.Fatalf("acceptation : %v", s.DraftRate)
}
// Par nature aussi : le sous-agent sans brouillon n'a pas de taux inventé.
if m.DraftRate == nil || *m.DraftRate != 0.75 || m.DraftN != 40 || m.DraftAccepted != 30 {
t.Fatalf("acceptation main : %+v", m)
}
if sa := s.Kinds[perfSubagent]; sa.DraftRate != nil || sa.DraftN != 0 {
t.Fatalf("acceptation sous-agent : %+v", sa)
}
if len(s.Depth) != 4 || s.Depth[0].PPMedianTPS == nil || *s.Depth[0].PPMedianTPS != 500 || s.Depth[2].TGMedianTPS == nil || *s.Depth[2].TGMedianTPS != 30 || s.Depth[3].N != 0 {
t.Fatalf("profondeurs : %+v", s.Depth)
}
+5 -3
View File
@@ -98,10 +98,12 @@ var configTemplate = []struct{ key, help string }{
{"CKPT_MIN_STEP", "espacement minimal en jetons entre deux points de reprise (--checkpoint-min-step), > 0 ; " +
"vide = défaut du moteur, ou 2048 d'office sur un hybride avec un moteur officiel antérieur à b10864"},
{"SPEC", "décodage spéculatif, sortie inchangée : off (défaut) / auto = tête MTP du modèle ou MODEL_DRAFT si --fit place " +
"tout et qu'aucun essai n'a échoué / mtp = imposé ; ~1-2 Go de VRAM en plus"},
{"MODEL_DRAFT", "tête MTP publiée à part (mtp-*.gguf) ou petit modèle brouillon, nom ou chemin comme MODEL ; utilisé si SPEC≠off"},
"tout et qu'aucun essai n'a échoué / mtp = imposé ; ~1-2 Go de VRAM en plus / ngram = n-grammes du contexte " +
"(ngram-mod 24/48/64, sans brouillon, utile en mode code, à mesurer ; refusé avec des poids sur CPU sauf " +
"OP_OFFLOAD_MIN_BATCH ≥ 66) / mtp+ngram = les deux, n-grammes seuls si pas de tête MTP"},
{"MODEL_DRAFT", "tête MTP publiée à part (mtp-*.gguf) ou petit modèle brouillon, nom ou chemin comme MODEL ; utilisé avec SPEC=auto, mtp ou mtp+ngram"},
{"SPEC_N_MAX", "jetons anticipés par étape (--spec-draft-n-max) ; vide = défaut du moteur (3)"},
{"SPEC_SAMPLING", "tirage du brouillon : greedy (défaut, exact) ; probabilistic seulement avec SPEC=mtp"},
{"SPEC_SAMPLING", "tirage du brouillon : greedy (défaut, exact) ; probabilistic seulement avec SPEC=mtp ou mtp+ngram"},
{"REASONING", "passthrough du mode raisonnement (on/auto/deepseek)"},
{"REASONING_PRESERVE", "on/off = garder ou non la réflexion des tours passés dans le gabarit (--reasoning-preserve) ; " +
"vide = défaut du moteur (on depuis b10763). Sans REASONING_ECHO, Loki ne renvoie pas cette réflexion : off rend " +
+15 -7
View File
@@ -3486,7 +3486,8 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<option value="bf16">bf16 (numérique différente de f16)</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : « auto » et « MTP » écrivent la clé SPEC,
<!-- Décodage spéculatif : « auto », « MTP », « n-grammes du contexte » et
« MTP + n-grammes » écrivent la clé SPEC,
que Loki traduit au lancement (tête MTP détectée dans le fichier,
garde-fous VRAM) ; les autres types vont tels quels dans
--spec-type. « non » écrit SPEC=off. Le nombre de jetons anticipés
@@ -3497,6 +3498,8 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<span class="pe-row-c"><span class="pe-selc"><select id="s-spec" onchange="onSpecType()">
<option value="">non</option>
<option value="auto">auto (MTP si le modèle en a une et que la VRAM suit)</option>
<option value="ngram">n-grammes du contexte (expérimental · mode code, à mesurer)</option>
<option value="mtp+ngram">MTP + n-grammes (expérimental · à mesurer)</option>
<optgroup label="Modèle brouillon">
<option value="draft-mtp">MTP (imposé)</option>
<option value="draft-eagle3">EAGLE-3</option>
@@ -6076,10 +6079,14 @@ function syncLoadModeSub(mode){
// « auto » et « MTP » passent par la clé SPEC : Loki cherche la tête MTP dans
// le fichier et pose les garde-fous au lancement. Les autres types restent des
// --spec-type d'EXTRA_ARGS, qui ont toujours le dernier mot côté serveur.
// « ngram » et « mtp+ngram » aussi : Loki écrit lui-même les bornes de
// ngram-mod et refuse là où les longs lots de vérification coûteraient cher.
function specFromKey(v){
v = String(v||'').trim().toLowerCase();
return v === 'auto' ? 'auto' : v === 'mtp' ? 'draft-mtp' : '';
if(v === 'ngram+mtp') v = 'mtp+ngram';
return v === 'auto' ? 'auto' : v === 'mtp' ? 'draft-mtp' : (v === 'ngram' || v === 'mtp+ngram') ? v : '';
}
const SPEC_KEY_VALUES = {'auto': 'auto', 'draft-mtp': 'mtp', 'ngram': 'ngram', 'mtp+ngram': 'mtp+ngram'};
// Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée
// par des virgules ; une valeur composée (ou un type sorti après cette version
// de loki) ne correspondrait à aucune option et serait silencieusement effacée
@@ -6104,20 +6111,21 @@ function syncSpecRow(){
const row = document.getElementById('s-spec-n-row');
if(!sel || !row) return;
const on = !!sel.value;
row.style.display = on ? '' : 'none';
// SPEC=ngram n'a pas de brouillon : un --spec-draft-n-max le ferait refuser.
row.style.display = on && sel.value !== 'ngram' ? '' : 'none';
// Le brouillon ne sert qu'à SPEC (auto / MTP) ; un brouillon déjà posé reste
// visible pour qu'on puisse le retirer.
const dr = document.getElementById('s-draft-row');
if(dr) dr.style.display = (sel.value === 'auto' || sel.value === 'draft-mtp' || cfgReadKey('MODEL_DRAFT')) ? '' : 'none';
if(dr) dr.style.display = (sel.value === 'auto' || sel.value === 'draft-mtp' || sel.value === 'mtp+ngram' || cfgReadKey('MODEL_DRAFT')) ? '' : 'none';
return on;
}
function onSpecType(){
const v = document.getElementById('s-spec').value;
const viaKey = v === 'auto' || v === 'draft-mtp';
const viaKey = v in SPEC_KEY_VALUES;
// « non » écrit SPEC=off : explicite, et identique au défaut du serveur.
cfgWriteKey('SPEC', v === 'auto' ? 'auto' : v === 'draft-mtp' ? 'mtp' : v ? '' : 'off');
cfgWriteKey('SPEC', viaKey ? SPEC_KEY_VALUES[v] : v ? '' : 'off');
eaSetValued('--spec-type', viaKey ? '' : v);
if(!v){
if(!v || v === 'ngram'){
eaSetValued('--spec-draft-n-max', '');
document.getElementById('s-spec-n').value = '';
}
+4 -1
View File
@@ -1260,7 +1260,8 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<option value="bf16">bf16 (numérique différente de f16)</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : « auto » et « MTP » écrivent la clé SPEC,
<!-- Décodage spéculatif : « auto », « MTP », « n-grammes du contexte » et
« MTP + n-grammes » écrivent la clé SPEC,
que Loki traduit au lancement (tête MTP détectée dans le fichier,
garde-fous VRAM) ; les autres types vont tels quels dans
--spec-type. « non » écrit SPEC=off. Le nombre de jetons anticipés
@@ -1271,6 +1272,8 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<span class="pe-row-c"><span class="pe-selc"><select id="s-spec" onchange="onSpecType()">
<option value="">non</option>
<option value="auto">auto (MTP si le modèle en a une et que la VRAM suit)</option>
<option value="ngram">n-grammes du contexte (expérimental · mode code, à mesurer)</option>
<option value="mtp+ngram">MTP + n-grammes (expérimental · à mesurer)</option>
<optgroup label="Modèle brouillon">
<option value="draft-mtp">MTP (imposé)</option>
<option value="draft-eagle3">EAGLE-3</option>
+11 -6
View File
@@ -1108,10 +1108,14 @@ function syncLoadModeSub(mode){
// « auto » et « MTP » passent par la clé SPEC : Loki cherche la tête MTP dans
// le fichier et pose les garde-fous au lancement. Les autres types restent des
// --spec-type d'EXTRA_ARGS, qui ont toujours le dernier mot côté serveur.
// « ngram » et « mtp+ngram » aussi : Loki écrit lui-même les bornes de
// ngram-mod et refuse là où les longs lots de vérification coûteraient cher.
function specFromKey(v){
v = String(v||'').trim().toLowerCase();
return v === 'auto' ? 'auto' : v === 'mtp' ? 'draft-mtp' : '';
if(v === 'ngram+mtp') v = 'mtp+ngram';
return v === 'auto' ? 'auto' : v === 'mtp' ? 'draft-mtp' : (v === 'ngram' || v === 'mtp+ngram') ? v : '';
}
const SPEC_KEY_VALUES = {'auto': 'auto', 'draft-mtp': 'mtp', 'ngram': 'ngram', 'mtp+ngram': 'mtp+ngram'};
// Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée
// par des virgules ; une valeur composée (ou un type sorti après cette version
// de loki) ne correspondrait à aucune option et serait silencieusement effacée
@@ -1136,20 +1140,21 @@ function syncSpecRow(){
const row = document.getElementById('s-spec-n-row');
if(!sel || !row) return;
const on = !!sel.value;
row.style.display = on ? '' : 'none';
// SPEC=ngram n'a pas de brouillon : un --spec-draft-n-max le ferait refuser.
row.style.display = on && sel.value !== 'ngram' ? '' : 'none';
// Le brouillon ne sert qu'à SPEC (auto / MTP) ; un brouillon déjà posé reste
// visible pour qu'on puisse le retirer.
const dr = document.getElementById('s-draft-row');
if(dr) dr.style.display = (sel.value === 'auto' || sel.value === 'draft-mtp' || cfgReadKey('MODEL_DRAFT')) ? '' : 'none';
if(dr) dr.style.display = (sel.value === 'auto' || sel.value === 'draft-mtp' || sel.value === 'mtp+ngram' || cfgReadKey('MODEL_DRAFT')) ? '' : 'none';
return on;
}
function onSpecType(){
const v = document.getElementById('s-spec').value;
const viaKey = v === 'auto' || v === 'draft-mtp';
const viaKey = v in SPEC_KEY_VALUES;
// « non » écrit SPEC=off : explicite, et identique au défaut du serveur.
cfgWriteKey('SPEC', v === 'auto' ? 'auto' : v === 'draft-mtp' ? 'mtp' : v ? '' : 'off');
cfgWriteKey('SPEC', viaKey ? SPEC_KEY_VALUES[v] : v ? '' : 'off');
eaSetValued('--spec-type', viaKey ? '' : v);
if(!v){
if(!v || v === 'ngram'){
eaSetValued('--spec-draft-n-max', '');
document.getElementById('s-spec-n').value = '';
}