mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
En --parallel 1, une vérification, un sous-agent, une tâche ou un bench prennent le slot de la discussion : son état part dans le cache RAM et en revient, ou se recalcule s'il n'y tient plus. Avec SIDE_SLOT=on (off par défaut), le moteur ouvre deux slots et la discussion garde le sien. Dimensionnement choisi pour qu'un débordement concurrent soit impossible par construction : cache KV NON unifié (--no-kv-unified) et -c 2×CTX, soit deux flux séparés de CTX jetons. Sous cache unifié, les deux slots partagent le pool et, plein, llama.cpp renvoie « Context size has been exceeded. » aux deux — que Loki prenait pour un débordement de la conversation. Chaque slot garde la fenêtre entière : ctxWindow et la compaction restent sur CTX, rien n'est raccourci. --cache-idle-slots ne vide les slots au repos que sous cache unifié : rien à régler. - lancement : refus (un slot, ligne d'avant, note au journal) si modèle + deux états dépassent 90 % de la VRAM, VRAM ou GGUF inconnus, poids sur CPU, moteur sans --no-kv-unified, PARALLEL≠2, ou -c/-np/-kvu/--no-kv-unified/ --kv-unified-per-slot/--cache-idle-slots dans EXTRA_ARGS (ou leurs LLAMA_ARG_*) ; note de VRAM en plus, avertissement si NGL imposé - routage id_slot seulement si /props annonce 2 slots d'au moins CTX jetons : 0 pour le tour, ses étapes, le préchauffage et le résumé en continuation ; 1 pour vérification, sous-agents, tâches, bench, résumé sur transcription - clients /v1 (proxy et relais) : corps réécrit en id_slot 1, quel qu'il soit - cohérence lot 1 : l'effacement de slot s'abstient (2 slots) ; une requête du slot 1 n'avance pas le numéro d'engineSlotHolds, la continuation de compaction reste possible après une vérification - « Context size has been exceeded. » sans nombre de jetons, deux slots en service : requête rejouée telle quelle, jamais compaction ni réduction - préchauffage permis sur les deux slots de SIDE_SLOT, vers le slot 0 - éditeur de preset : VRAM du second slot ou raison du refus - tests : ligne identique sans la clé, chaque conflit refuse sans changer la ligne, routage par nature, aucun id_slot sur un slot / preset externe / fenêtre partagée, rejeu sans compaction, proxy forcé, préchauffage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
5.3 KiB
Go
148 lines
5.3 KiB
Go
package loki
|
||
|
||
import (
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// helpSide : un moteur qui connaît le cache KV non unifié.
|
||
const helpSide = helpCacheRAM + `-kvu, --kv-unified, -no-kvu, --no-kv-unified
|
||
use single unified KV buffer shared across all sequences
|
||
`
|
||
|
||
// sideCfg : le preset de denseGPU (0,25 Mio d'état par jeton) à 32k, avec ou
|
||
// sans SIDE_SLOT.
|
||
func sideCfg(kv ...string) map[string]string {
|
||
cfg := map[string]string{"CTX": "32768", "NGL": "999"}
|
||
for i := 0; i+1 < len(kv); i += 2 {
|
||
cfg[kv[i]] = kv[i+1]
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
func sideSI() serveSysInfo {
|
||
si := denseGPU()
|
||
si.Help = helpSide
|
||
return si
|
||
}
|
||
|
||
// Sans la clé (ou à off), la ligne de commande est celle d'avant, à l'octet
|
||
// près, notes comprises.
|
||
func TestSideSlotDefautIdentique(t *testing.T) {
|
||
for _, v := range []string{"", "off", "0"} {
|
||
cfg := sideCfg()
|
||
if v != "" {
|
||
cfg["SIDE_SLOT"] = v
|
||
}
|
||
got, _, notes := buildServeArgs(cfg, nil, "/bin/llama-server", sideSI())
|
||
want, _, wantNotes := buildServeArgs(sideCfg(), nil, "/bin/llama-server", sideSI())
|
||
if !reflect.DeepEqual(got, want) || !reflect.DeepEqual(notes, wantNotes) {
|
||
t.Fatalf("SIDE_SLOT=%q change la ligne :\n%v\n%v", v, got, want)
|
||
}
|
||
if argValue(got, "--parallel") != "1" || argValue(got, "-c") != "32768" {
|
||
t.Fatalf("défaut : %v", got)
|
||
}
|
||
for _, a := range got {
|
||
if a == "--no-kv-unified" {
|
||
t.Fatal("--no-kv-unified sans la clé")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Accepté : deux slots de CTX jetons chacun, flux KV séparés — le pool fait
|
||
// le double, aucun slot ne peut déborder sur l'autre.
|
||
func TestSideSlotAccepte(t *testing.T) {
|
||
for _, cfg := range []map[string]string{sideCfg("SIDE_SLOT", "on"), sideCfg("SIDE_SLOT", "on", "PARALLEL", "2")} {
|
||
args, _, notes := buildServeArgs(cfg, nil, "/bin/llama-server", sideSI())
|
||
if argValue(args, "-c") != "65536" || argValue(args, "--parallel") != "2" {
|
||
t.Fatalf("args = %v", args)
|
||
}
|
||
n := 0
|
||
for i, a := range args {
|
||
if a == "--no-kv-unified" {
|
||
n++
|
||
if args[i-1] != "2" || args[i-2] != "--parallel" {
|
||
t.Fatalf("--no-kv-unified mal placé : %v", args)
|
||
}
|
||
}
|
||
if a == "--kv-unified-per-slot" || a == "-kvu" || a == "--kv-unified" {
|
||
t.Fatalf("drapeau inattendu %s", a)
|
||
}
|
||
}
|
||
if n != 1 {
|
||
t.Fatalf("--no-kv-unified × %d", n)
|
||
}
|
||
found := false
|
||
for _, s := range notes {
|
||
if strings.HasPrefix(s, "SIDE_SLOT : 2 slots de 32768 jetons") && strings.Contains(s, "~8192 Mio") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("note de VRAM absente : %q", notes)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Refusé : la ligne est exactement celle sans la clé, plus une note qui dit
|
||
// pourquoi.
|
||
func TestSideSlotRefus(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
cfg map[string]string
|
||
extra []string
|
||
si func(*serveSysInfo)
|
||
why string
|
||
}{
|
||
{name: "PARALLEL=3", cfg: sideCfg("SIDE_SLOT", "on", "PARALLEL", "3"), why: "PARALLEL=3"},
|
||
{name: "-c dans EXTRA_ARGS", extra: []string{"-c", "16384"}, why: "-c "},
|
||
{name: "--ctx-size=", extra: []string{"--ctx-size=16384"}, why: "--ctx-size"},
|
||
{name: "-np", extra: []string{"-np", "4"}, why: "-np"},
|
||
{name: "-kvu", extra: []string{"-kvu"}, why: "-kvu"},
|
||
{name: "--no-kv-unified", extra: []string{"--no-kv-unified"}, why: "--no-kv-unified"},
|
||
{name: "--kv-unified-per-slot", extra: []string{"--kv-unified-per-slot", "8192"}, why: "--kv-unified-per-slot"},
|
||
{name: "--cache-idle-slots", extra: []string{"--cache-idle-slots"}, why: "--cache-idle-slots"},
|
||
{name: "--no-cache-idle-slots", extra: []string{"--no-cache-idle-slots"}, why: "--no-cache-idle-slots"},
|
||
{name: "variable LLAMA_ARG_N_PARALLEL", si: func(si *serveSysInfo) { si.ArgEnv["LLAMA_ARG_N_PARALLEL"] = "4" }, why: "LLAMA_ARG_N_PARALLEL"},
|
||
{name: "variable LLAMA_ARG_KV_UNIFIED", si: func(si *serveSysInfo) { si.ArgEnv["LLAMA_ARG_KV_UNIFIED"] = "1" }, why: "LLAMA_ARG_KV_UNIFIED"},
|
||
{name: "moteur sans --no-kv-unified", si: func(si *serveSysInfo) { si.Help = helpCacheRAM }, why: "--no-kv-unified"},
|
||
{name: "CTX=0", cfg: sideCfg("SIDE_SLOT", "on", "CTX", "0"), why: "CTX"},
|
||
{name: "VRAM trop juste à 64k", cfg: sideCfg("SIDE_SLOT", "on", "CTX", "65536"), why: "VRAM insuffisante"},
|
||
{name: "VRAM inconnue", si: func(si *serveSysInfo) { si.VRAMMiB = 0 }, why: "VRAM"},
|
||
{name: "GGUF illisible", si: func(si *serveSysInfo) { si.GGUF = nil }, why: "GGUF"},
|
||
{name: "experts sur CPU", extra: []string{"--n-cpu-moe", "20"}, why: "CPU"},
|
||
{name: "-ot sur CPU", extra: []string{"-ot", "exps=CPU"}, why: "CPU"},
|
||
{name: "NGL partiel", cfg: sideCfg("SIDE_SLOT", "on", "NGL", "30"), why: "couches GPU"},
|
||
}
|
||
for _, c := range cases {
|
||
cfg := c.cfg
|
||
if cfg == nil {
|
||
cfg = sideCfg("SIDE_SLOT", "on")
|
||
}
|
||
si := sideSI()
|
||
if c.si != nil {
|
||
c.si(&si)
|
||
}
|
||
_, _, why := sideSlotPlan(cfg, c.extra, si)
|
||
if !strings.Contains(why, c.why) {
|
||
t.Errorf("%s : why = %q, attendu %q", c.name, why, c.why)
|
||
}
|
||
got, _, notes := buildServeArgs(cfg, c.extra, "/bin/llama-server", si)
|
||
off := map[string]string{}
|
||
for k, v := range cfg {
|
||
if k != "SIDE_SLOT" {
|
||
off[k] = v
|
||
}
|
||
}
|
||
want, _, wantNotes := buildServeArgs(off, c.extra, "/bin/llama-server", si)
|
||
if !reflect.DeepEqual(got, want) {
|
||
t.Errorf("%s : refus qui change la ligne\n%v\n%v", c.name, got, want)
|
||
}
|
||
if len(notes) != len(wantNotes)+1 {
|
||
t.Errorf("%s : notes %q", c.name, notes)
|
||
}
|
||
}
|
||
}
|