Files
llmgw/internal/control/modelserve/context_test.go
T
Ilker ManapandClaude Opus 5.5 31f440ab6e
CI / test (push) Successful in 9m29s
A served model's longest answer changed in place, and Strata's context
max_output on POST /api/v1/models/{name}/context, --max-output on ctl model
context, and a field in the console's Context length… form. Alone it changes
only the catalog's limit, which the router holds max_tokens to, and starts no
replica again; with a context it is set with it, never past it.

Strata's context is now changed like the others', through its setup's
--context, which writes the engine's start script at each start. Neither
needs a model removed and added again any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:15:38 +01:00

145 lines
5.7 KiB
Go

package modelserve
import (
"context"
"slices"
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
)
func TestAContextChangeResizesTheEngineAndTheMemoryAndRestartsTheReplicas(t *testing.T) {
s, _, models, _, fleet := updSetup(t)
m := models.models["qwen"]
m.ContextLength, m.MaxOutput = 4096, maxOutput(4096)
m.Origin = &catalog.Origin{Repo: "o/r", Files: []catalog.OriginFile{{Path: "q4.gguf", Bytes: 2 << 30}}}
m.Profiles[0].Args = []string{"--ctx-size", "8192", "--parallel", "2"}
models.models["qwen"] = m
got, err := s.SetContext(context.Background(), "qwen", ContextRequest{Tokens: 32768}, "admin")
if err != nil {
t.Fatal(err)
}
if got.Before != 4096 || got.After != 32768 || got.Restarted != 2 || got.Note != "" {
t.Fatalf("%+v", got)
}
after := models.models["qwen"]
if after.ContextLength != 32768 || after.MaxOutput != maxOutput(32768) {
t.Errorf("the catalog says %d tokens, %d out", after.ContextLength, after.MaxOutput)
}
// Two slots kept: the pool is both.
if a := after.Profiles[0].Args; !slices.Equal(a, []string{"--ctx-size", "65536", "--parallel", "2"}) {
t.Errorf("args %v", a)
}
// 2 GiB of weights, 64k tokens at 28 KiB each, an engine's share.
if gb := after.Profiles[0].MinMemoryGB; gb < 4 || gb > 6 {
t.Errorf("memory %d GB", gb)
}
for _, r := range fleet.Replicas() {
if !strings.Contains(r.ID, "-new") {
t.Errorf("%s was not started again", r.ID)
}
}
}
func TestAContextTheCardsCannotHoldIsRefusedWithTheFigures(t *testing.T) {
sh := shape{kvPerToken: 128 << 10, weights: 8 << 30}
p := catalog.Profile{Name: "gpu", Engine: "vllm", GPUClass: "t4-16g", GPUsPerNode: 1}
if _, _, err := resizeProfile(p, ContextRequest{Tokens: 131072}, sh); err == nil || !strings.Contains(err.Error(), "GB") {
t.Fatalf("not refused: %v", err)
}
next, change, err := resizeProfile(p, ContextRequest{Tokens: 4096}, sh)
if err != nil {
t.Fatal(err)
}
if !slices.Equal(next.Args, []string{"--max-model-len", "4096"}) || !strings.Contains(change.After, "a card of 1") {
t.Errorf("%v %+v", next.Args, change)
}
}
func TestAContextIsRefusedForADeclaredModelOrPastWhatItWasTrainedFor(t *testing.T) {
s, _, models, _, _ := updSetup(t)
models.declared["qwen"] = true
if _, err := s.SetContext(context.Background(), "qwen", ContextRequest{Tokens: 8192}, "admin"); err == nil {
t.Error("a declared model's context was changed")
}
models.declared["qwen"] = false
if _, err := s.SetContext(context.Background(), "qwen", ContextRequest{Tokens: 100}, "admin"); err == nil {
t.Error("100 tokens taken")
}
if _, err := s.SetContext(context.Background(), "nope", ContextRequest{Tokens: 8192}, "admin"); err == nil {
t.Error("no such model, and no error")
}
}
func TestEngineArgumentsAreSetInEitherSpelling(t *testing.T) {
if a := setArg([]string{"--ctx-size=4096", "-ngl", "99"}, "--ctx-size", "8192"); !slices.Equal(a, []string{"--ctx-size=8192", "-ngl", "99"}) {
t.Error(a)
}
if a := setArg([]string{"-ngl", "99"}, "--parallel", "4"); !slices.Equal(a, []string{"-ngl", "99", "--parallel", "4"}) {
t.Error(a)
}
if argInt([]string{"--parallel=3"}, "--parallel") != 3 || argInt(nil, "--parallel") != 0 {
t.Error("argInt")
}
p := catalog.Profile{Name: "x", Engine: "comfyui"}
if _, _, err := resizeProfile(p, ContextRequest{Tokens: 4096}, shape{}); err == nil {
t.Error("a comfyui profile took a context")
}
}
func TestTheLongestAnswerAloneChangesOnlyTheCatalogAndStartsNothing(t *testing.T) {
s, _, models, _, fleet := updSetup(t)
m := models.models["qwen"]
m.ContextLength, m.MaxOutput = 4096, 1024
m.Profiles[0].Args = []string{"--ctx-size", "4096"}
models.models["qwen"] = m
before := fleet.Replicas()
got, err := s.SetContext(context.Background(), "qwen", ContextRequest{MaxOutput: 3000}, "admin")
if err != nil {
t.Fatal(err)
}
if !got.OutputOnly || got.MaxOutput != 3000 || got.MaxOutputBefore != 1024 || got.Restarted != 0 {
t.Fatalf("%+v", got)
}
after := models.models["qwen"]
if after.ContextLength != 4096 || after.MaxOutput != 3000 || !slices.Equal(after.Profiles[0].Args, []string{"--ctx-size", "4096"}) {
t.Errorf("catalog %d/%d, args %v", after.ContextLength, after.MaxOutput, after.Profiles[0].Args)
}
for i, r := range fleet.Replicas() {
if r.ID != before[i].ID {
t.Errorf("%s was started again for a limit no engine reads", before[i].ID)
}
}
// An answer longer than the context is refused, alone or with one.
if _, err := s.SetContext(context.Background(), "qwen", ContextRequest{MaxOutput: 5000}, "admin"); err == nil {
t.Error("an answer longer than the context was accepted")
}
if _, err := s.SetContext(context.Background(), "qwen", ContextRequest{Tokens: 8192, MaxOutput: 9000}, "admin"); err == nil {
t.Error("an answer longer than the new context was accepted")
}
// With a context, the one asked for wins over the one that follows it.
got, err = s.SetContext(context.Background(), "qwen", ContextRequest{Tokens: 8192, MaxOutput: 6000}, "admin")
if err != nil || got.MaxOutput != 6000 || models.models["qwen"].MaxOutput != 6000 {
t.Errorf("%+v %v", got, err)
}
}
func TestStrataTakesItsContextInItsSetupArguments(t *testing.T) {
p := catalog.Profile{Name: "one-card", Engine: "strata", GPUClass: "6000-24g",
Args: []string{"--family", "qwen", "--size", "IQ2_XS"}}
got, change, err := resizeProfile(p, ContextRequest{Tokens: 32768}, shape{kvPerToken: 1 << 20, weights: 40 << 30})
if err != nil {
t.Fatal(err)
}
// Not refused for weights larger than the card: its experts live in RAM.
if !slices.Equal(got.Args, []string{"--family", "qwen", "--size", "IQ2_XS", "--context", "32768"}) || !slices.Equal(change.Args, got.Args) {
t.Errorf("args %v", got.Args)
}
if n := contextOf(catalog.Model{ContextLength: 4096}, got); n != 32768 {
t.Errorf("contextOf %d", n)
}
}