mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 05:07:20 +02:00
Avec LOKI_BENCH=rust (expérimental), la façade sert /api/bench, /api/bench/status, /api/bench/cancel et /api/bench/last, et déroule le protocole de mesure contre le moteur local (rust/loki-rs/src/bench) : mode rapide (préchauffage, ligne courte) et mode complet (prefill à froid à profondeur réelle, trois tours qui reprennent le cache). Corpus, requêtes, calculs et messages sont ceux du Go ; seule une mesure complète est enregistrée (last_bench, bench_presets, empreinte de configuration du Go). Le verrou de génération reste celui de la conversation du Go : la façade le prend par le bail du moteur, avec le motif « bench ». Le Go note alors benching et donne aux autres le refus du benchmark ; changer de discussion ne rompt pas le bail ; benchRunning() le voit (clients /v1, préchauffage, optimiseur). Un stop du chat, un Reset ou un bail perdu annulent le bench. Le Go prête aussi le travail annexe (effacement du slot à la fin, /internal/v1/engine/sidejob) et la télémétrie (/internal/v1/bench/perf). Correction des tests du Go : trois tests du préchauffage échouaient dix minutes par jour, avant minuit (prewarmNearMidnight lisait l'heure réelle). L'heure du garde-fou devient remplaçable (prewarmNow), fixée à midi dans les tests. Parité : parity_bench_test.go tire du Go un journal rejoué par tests/bench.rs contre le même faux moteur ; chaque requête reçue par le moteur est comparée (longs textes réduits à leur taille et à leur empreinte). Il couvre les refus, les deux modes, les échecs de chaque phase, le raisonnement refusé puis rejoué, la profondeur partielle et l'annulation. Les tests du Go sont portés (bench/tests.rs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
168 lines
5.9 KiB
Go
168 lines
5.9 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Le banc d'essai part à la façade avec LOKI_BENCH=rust (lot 27) ; le reste,
|
|
// non. La façade tient le verrou de génération par le bail du moteur, motif
|
|
// « bench », et le Go lui prête le travail annexe et la télémétrie.
|
|
func TestFacadeBench(t *testing.T) {
|
|
testHome(t)
|
|
var mu sync.Mutex
|
|
var calls []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get(internalTokenHeader) != jetonTest {
|
|
w.WriteHeader(403)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
calls = append(calls, r.Method+" "+r.URL.Path)
|
|
mu.Unlock()
|
|
io.WriteString(w, `{"ok":true,"de":"facade"}`)
|
|
}))
|
|
defer srv.Close()
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
t.Setenv(facadeAddrEnv, strings.TrimPrefix(srv.URL, "http://"))
|
|
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
|
|
|
|
t.Setenv(facadeBenchEnv, "")
|
|
if facadeBench() {
|
|
t.Fatal("tenu sans LOKI_BENCH")
|
|
}
|
|
for p := range facadeBenchRoutes {
|
|
w := httptest.NewRecorder()
|
|
withFacadeChat(p, local)(w, httptest.NewRequest("POST", p, strings.NewReader("{}")))
|
|
if w.Body.String() != "go" {
|
|
t.Fatalf("sans LOKI_BENCH, %s : %q", p, w.Body.String())
|
|
}
|
|
}
|
|
t.Setenv(facadeBenchEnv, " RUST")
|
|
if len(facadeBenchRoutes) != 4 {
|
|
t.Fatalf("routes : %v", facadeBenchRoutes)
|
|
}
|
|
for p := range facadeBenchRoutes {
|
|
w := httptest.NewRecorder()
|
|
withFacadeChat(p, local)(w, httptest.NewRequest("POST", p, strings.NewReader("{}")))
|
|
if !strings.Contains(w.Body.String(), "facade") {
|
|
t.Fatalf("%s non relayé : %q", p, w.Body.String())
|
|
}
|
|
}
|
|
mu.Lock()
|
|
n := len(calls)
|
|
mu.Unlock()
|
|
if n != len(facadeBenchRoutes) {
|
|
t.Fatalf("relais : %v", calls)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
withFacadeChat("/api/tune", local)(w, httptest.NewRequest("POST", "/api/tune", nil))
|
|
if w.Body.String() != "go" {
|
|
t.Fatalf("l'optimiseur reste au Go : %q", w.Body.String())
|
|
}
|
|
|
|
// Le bail « bench » : le verrou, le refus du bench pour les autres.
|
|
post := func(h http.HandlerFunc, path, body string) (int, map[string]any) {
|
|
rec := httptest.NewRecorder()
|
|
h(rec, httptest.NewRequest("POST", path, strings.NewReader(body)))
|
|
var out map[string]any
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
|
return rec.Code, out
|
|
}
|
|
code, out := post(handleInternalLease, "/internal/v1/engine/lease", `{"ttl_ms":60000,"why":"bench"}`)
|
|
if code != 200 {
|
|
t.Fatalf("bail : %d %v", code, out)
|
|
}
|
|
id, _ := out["lease"].(string)
|
|
if !benchRunning() || conv.busyReason() != errBenchBusy {
|
|
t.Fatalf("bail du bench : running=%v refus=%v", benchRunning(), conv.busyReason())
|
|
}
|
|
if code, out := post(handleInternalLease, "/internal/v1/engine/lease", `{}`); code != 409 || out["error"] != errBenchBusy.Error() {
|
|
t.Fatalf("second bail : %d %v", code, out)
|
|
}
|
|
// Changer de discussion ne rompt pas le bail d'un bench.
|
|
conv.loadFrom("autre", nil)
|
|
if !benchRunning() || !conv.isGenerating() {
|
|
t.Fatal("le bail du bench a sauté à la bascule de discussion")
|
|
}
|
|
if code, _ := post(handleInternalLeaseRelease, "/internal/v1/engine/lease/release", `{"lease":"`+id+`"}`); code != 204 {
|
|
t.Fatalf("libération : %d", code)
|
|
}
|
|
if benchRunning() || conv.isGenerating() || conv.busyReason() == errBenchBusy {
|
|
t.Fatal("bail rendu : le verrou devait être libre")
|
|
}
|
|
// Un bail ordinaire n'est pas un bench ; un bail de bench expiré rend
|
|
// le verrou et le refus.
|
|
code, out = post(handleInternalLease, "/internal/v1/engine/lease", `{}`)
|
|
if code != 200 || benchRunning() {
|
|
t.Fatalf("bail ordinaire : %d %v bench=%v", code, out, benchRunning())
|
|
}
|
|
post(handleInternalLeaseRelease, "/internal/v1/engine/lease/release", `{"lease":"`+out["lease"].(string)+`"}`)
|
|
conv.mu.Lock()
|
|
conv.extLeaseClearLocked()
|
|
conv.Generating = false
|
|
conv.mu.Unlock()
|
|
if _, err := conv.extLeaseAcquire(5*time.Second, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv.mu.Lock()
|
|
st := conv.extLease
|
|
conv.mu.Unlock()
|
|
conv.extLeaseExpire(st)
|
|
if benchRunning() || conv.busyReason() == errBenchBusy {
|
|
t.Fatal("bail de bench expiré : refus du bench encore là")
|
|
}
|
|
|
|
// Le travail annexe ouvert puis fermé par la façade.
|
|
code, out = post(handleInternalSideJob, "/internal/v1/engine/sidejob", `{}`)
|
|
job, _ := out["id"].(float64)
|
|
if code != 200 || job <= 0 || sideJobs.Load() != 1 {
|
|
t.Fatalf("ouverture : %d %v (en cours : %d)", code, out, sideJobs.Load())
|
|
}
|
|
served := engineServedCount()
|
|
if code, _ := post(handleInternalSideJob, "/internal/v1/engine/sidejob", `{"end":`+strconv.Itoa(int(job))+`,"served":true}`); code != 204 {
|
|
t.Fatalf("fermeture : %d", code)
|
|
}
|
|
if sideJobs.Load() != 0 || engineServedCount() != served+1 {
|
|
t.Fatalf("après la fermeture : en cours %d, servies %d", sideJobs.Load(), engineServedCount()-served)
|
|
}
|
|
if code, _ := post(handleInternalSideJob, "/internal/v1/engine/sidejob", `{"end":999}`); code != 404 {
|
|
t.Fatalf("travail inconnu : %d", code)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
handleInternalSideJob(rec, httptest.NewRequest("GET", "/internal/v1/engine/sidejob", nil))
|
|
if rec.Code != 405 {
|
|
t.Fatalf("GET : %d", rec.Code)
|
|
}
|
|
|
|
// La trace du bench dans la télémétrie.
|
|
var before uint64
|
|
if recs := perfLog.snapshot(); len(recs) > 0 {
|
|
before = recs[len(recs)-1].Seq
|
|
}
|
|
if code, _ := post(handleInternalBenchPerf, "/internal/v1/bench/perf",
|
|
`{"prompt_n":2036,"prompt_ms":1018,"prompt_per_second":2000,"predicted_n":300,"predicted_per_second":50}`); code != 204 {
|
|
t.Fatalf("trace : %d", code)
|
|
}
|
|
recs := perfLog.snapshot()
|
|
if len(recs) == 0 || recs[len(recs)-1].Seq != before+1 {
|
|
t.Fatalf("trace absente (dernière avant : %d)", before)
|
|
}
|
|
r := recs[len(recs)-1]
|
|
if r.Kind != perfBench || !r.Complete || r.Total != 2036 || r.New != 2036 || r.Gen != 300 || r.TGtps != 50 || r.PPtps != 2000 || r.PPms != 1018 {
|
|
t.Fatalf("trace : %+v", r)
|
|
}
|
|
|
|
t.Setenv(internalTokenEnv, "")
|
|
if facadeBench() {
|
|
t.Fatal("tenu sans jeton interne")
|
|
}
|
|
}
|