Bench : corrections de relecture — niveau de raisonnement, préchauffage borné, verrou fiable

La relecture du bench en tâche de fond a trouvé quatre défauts qui le
faisaient échouer ou s'interrompre là où il n'aurait pas dû, et une
course entre la fin annoncée et le verrou rendu.

- Niveau de raisonnement refusé par le gabarit (Qwen3.8 et « high ») :
  le bench rejoue une fois avec le niveau accepté, comme runChatTools,
  et retient la traduction. Il échouait sur ce 500 dans un processus
  neuf. L'erreur HTTP garde le corps entier : les niveaux acceptés sont
  cités au-delà des 300 caractères affichés.
- Préchauffage borné au quart du contexte : -ub 4096 sur 8k de contexte
  envoyait un prompt plus grand que la fenêtre.
- Changer de discussion n'annule plus le bench, et ne lui reprend pas
  le verrou. La libération suit le drapeau benching au lieu de l'epoch,
  qu'une bascule de fil bumpe. Reset arrête toujours le bench et rend
  le verrou.
- Le verrou du chat est rendu avant que le statut ne dise « terminé » :
  un message envoyé aussitôt n'est plus refusé (et le test n'est plus
  instable).
- Indication « possible thrash » mesurée sur les tours seulement, pas
  sur le prefill à froid qui lit légitimement les pages du modèle.
- Interface : agrégats absents (omitempty) affichés à 0 au lieu de
  casser le rendu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 03:09:01 +02:00
1 parent cf83f5f629
commit 09ee04037b
6 files changed
+206 -42

No files matched your search

+104 -8
View File
@@ -18,14 +18,15 @@ import (
// de jetons à 4 octets par jeton, un cache qui reprend le prompt précédent
// quand cache_prompt est vrai, et le journal des requêtes de chat reçues.
type fakeBenchEngine struct {
mu sync.Mutex
nCtx int
busy bool
failAt int // n° (1-based) de la requête de chat qui répond 500 ; 0 = aucune
noTiming bool // réponses sans timings
slowHot time.Duration // délai des tours qui reprennent le cache
prev int
calls []map[string]any
mu sync.Mutex
nCtx int
busy bool
failAt int // n° (1-based) de la requête de chat qui répond 500 ; 0 = aucune
noTiming bool // réponses sans timings
rejectEffort string // niveau de reasoning_effort que le « gabarit » refuse (500)
slowHot time.Duration // délai des tours qui reprennent le cache
prev int
calls []map[string]any
}
func (f *fakeBenchEngine) handler(t *testing.T) http.Handler {
@@ -57,6 +58,13 @@ func (f *fakeBenchEngine) handler(t *testing.T) http.Handler {
http.Error(w, `{"error":"boom"}`, http.StatusInternalServerError)
return
}
if e, _ := p["reasoning_effort"].(string); f.rejectEffort != "" && e == f.rejectEffort {
// Le vrai message cite la trace jinja d'abord : les niveaux acceptés
// arrivent bien après les 300 premiers caractères.
http.Error(w, `{"error":{"code":500,"message":"`+strings.Repeat("jinja trace ", 40)+
`Unexpected reasoning effort `+e+`. Supported types are xhigh (default), medium, and low."}}`, http.StatusInternalServerError)
return
}
cache, _ := p["cache_prompt"].(bool)
if d := f.slowHot; cache && d > 0 {
select {
@@ -541,3 +549,91 @@ func TestBenchRefuseV1(t *testing.T) {
t.Fatalf("pendant le bench : %d, Retry-After=%q", rec.Code, rec.Header().Get("Retry-After"))
}
}
// Un gabarit qui refuse le niveau de raisonnement du preset : le bench rejoue
// une fois avec le niveau accepté, comme le chat, et le retient — au lieu
// d'échouer sur le 500.
func TestBenchNiveauRaisonnementRefuse(t *testing.T) {
f := &fakeBenchEngine{nCtx: 16384, rejectEffort: "high"}
eng, _ := f.start(t)
s := benchSetupFrom(map[string]string{"REASONING_EFFORT": "high"}, nil)
var learned []string
s.learn = func(want, got string) { learned = append(learned, want+">"+got) }
res, err := benchRun(context.Background(), eng, benchOpts{Mode: benchModeQuick}, s, testBenchCorpora(), nil)
if err != nil {
t.Fatalf("refus du niveau non rattrapé : %v", err)
}
if len(f.calls) != 3 || res.PredictedPerSec != 50 {
t.Fatalf("%d requêtes (attendu 3 : refus, préchauffage rejoué, ligne courte)", len(f.calls))
}
for i, p := range f.calls[1:] {
kw, _ := p["chat_template_kwargs"].(map[string]any)
if p["reasoning_effort"] != "xhigh" || kw["reasoning_effort"] != "xhigh" {
t.Errorf("requête %d : niveau %v / %v, attendu xhigh", i+2, p["reasoning_effort"], kw)
}
}
if len(learned) != 1 || learned[0] != "high>xhigh" {
t.Errorf("traduction retenue : %v", learned)
}
// Une autre erreur que ce refus n'est pas rejouée.
f2 := &fakeBenchEngine{nCtx: 16384, failAt: 1}
eng2, _ := f2.start(t)
if _, err := benchRun(context.Background(), eng2, benchOpts{}, benchSetupFrom(map[string]string{"REASONING_EFFORT": "high"}, nil), testBenchCorpora(), nil); err == nil || len(f2.calls) != 1 {
t.Errorf("500 ordinaire : err=%v, %d requêtes", err, len(f2.calls))
}
}
// Un gros ubatch sur un petit contexte : le préchauffage reste sous le quart
// de la fenêtre au lieu de la dépasser.
func TestBenchPrechauffageBorne(t *testing.T) {
f := &fakeBenchEngine{nCtx: 8192}
eng, _ := f.start(t)
s := benchSetupFrom(map[string]string{"UBATCH": "4096"}, nil)
if _, err := benchRun(context.Background(), eng, benchOpts{}, s, testBenchCorpora(), nil); err != nil {
t.Fatal(err)
}
c := f.calls[0]["messages"].([]any)[0].(map[string]any)["content"].(string)
if toks := len(c) / 4; toks > 8192/4+64 {
t.Errorf("préchauffage de %d jetons sur 8192 de contexte", toks)
}
}
// Changer de discussion n'arrête pas le bench et ne rend pas le verrou ; un
// Reset l'arrête et le rend, et la libération tardive du bench ne déclare pas
// libre le tour qui a démarré depuis.
func TestBenchVerrouEtDiscussions(t *testing.T) {
testHome(t)
c := newTestConv()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
release, err := c.benchLease(cancel)
if err != nil {
t.Fatal(err)
}
c.loadFrom("autre", nil)
if ctx.Err() != nil || !c.isGenerating() || c.busyReason() != errBenchBusy {
t.Fatal("changer de discussion a arrêté le bench ou rendu son verrou")
}
release()
if c.isGenerating() {
t.Fatal("verrou non rendu à la fin du bench, après un changement de discussion")
}
ctx2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
release2, err := c.benchLease(cancel2)
if err != nil {
t.Fatal(err)
}
c.Reset()
if ctx2.Err() == nil || c.isGenerating() {
t.Fatal("Reset doit arrêter le bench et rendre le verrou")
}
c.mu.Lock()
c.Generating = true // un tour démarré depuis
c.mu.Unlock()
release2()
if !c.isGenerating() {
t.Fatal("la libération tardive du bench a déclaré libre le tour suivant")
}
}