Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4933af12db |
+9
-8
@@ -246,14 +246,15 @@ func run(args []string) error {
|
|||||||
phr = phraser.NewStub()
|
phr = phraser.NewStub()
|
||||||
if cfg.Phraser != nil {
|
if cfg.Phraser != nil {
|
||||||
pc := phraser.Config{
|
pc := phraser.Config{
|
||||||
ModelPath: cfg.Phraser.ModelPath,
|
ModelPath: cfg.Phraser.ModelPath,
|
||||||
BinPath: cfg.Phraser.BinPath,
|
BinPath: cfg.Phraser.BinPath,
|
||||||
Listen: cfg.Phraser.Listen,
|
Listen: cfg.Phraser.Listen,
|
||||||
NGpuLayers: cfg.Phraser.NGpuLayers,
|
NGpuLayers: cfg.Phraser.NGpuLayers,
|
||||||
NCtx: cfg.Phraser.NCtx,
|
NCtx: cfg.Phraser.NCtx,
|
||||||
Timeout: time.Duration(cfg.Phraser.Timeout),
|
Timeout: time.Duration(cfg.Phraser.Timeout),
|
||||||
LLMNudges: cfg.Phraser.LLMNudges,
|
StartupTimeout: time.Duration(cfg.Phraser.StartupTimeout),
|
||||||
ContextBlock: contextBlockFn(cfg, time.Now),
|
LLMNudges: cfg.Phraser.LLMNudges,
|
||||||
|
ContextBlock: contextBlockFn(cfg, time.Now),
|
||||||
}
|
}
|
||||||
if pc.BinPath == "" {
|
if pc.BinPath == "" {
|
||||||
pc.BinPath = "llama-server"
|
pc.BinPath = "llama-server"
|
||||||
|
|||||||
@@ -1233,6 +1233,13 @@ type PhraserConfig struct {
|
|||||||
NCtx int `json:"n_ctx,omitempty"`
|
NCtx int `json:"n_ctx,omitempty"`
|
||||||
Timeout Duration `json:"timeout,omitempty"`
|
Timeout Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
// StartupTimeout — how long the daemon waits for llama-server to print its
|
||||||
|
// listen line at boot. Zero ⇒ the phraser's 60s default.
|
||||||
|
//
|
||||||
|
// It is here because a cold 1.7B loading off a spinning disk can outrun 60s,
|
||||||
|
// and that failed the boot with no way to raise it (Vikunja #323).
|
||||||
|
StartupTimeout Duration `json:"startup_timeout,omitempty"`
|
||||||
|
|
||||||
// LLMNudges — let the model word nudges again. Off by default: nudges are
|
// LLMNudges — let the model word nudges again. Off by default: nudges are
|
||||||
// worded from hand-written Russian templates now (the model broke the
|
// worded from hand-written Russian templates now (the model broke the
|
||||||
// persona and invented units). Chat, query and reminder phrasing always go
|
// persona and invented units). Chat, query and reminder phrasing always go
|
||||||
|
|||||||
@@ -101,8 +101,21 @@ type Config struct {
|
|||||||
// ever fights the grammar, the fix should be a config flip on the
|
// ever fights the grammar, the fix should be a config flip on the
|
||||||
// deploy box, not a code change and a rebuild.
|
// deploy box, not a code change and a rebuild.
|
||||||
NoGrammar bool
|
NoGrammar bool
|
||||||
|
|
||||||
|
// StartupTimeout — how long to wait for llama-server to print its listen
|
||||||
|
// line before giving up, killing the child and returning an error.
|
||||||
|
//
|
||||||
|
// A field rather than a constant because the box may legitimately want
|
||||||
|
// longer: a cold 1.7B loading off a spinning disk can outrun the 60s
|
||||||
|
// default, and that used to fail the boot with no way to raise it
|
||||||
|
// (Vikunja #323). Zero ⇒ defaultStartupTimeout.
|
||||||
|
StartupTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// defaultStartupTimeout — the wait DefaultConfig sets and the zero value falls
|
||||||
|
// back to, so a Config built by hand still has a ceiling.
|
||||||
|
const defaultStartupTimeout = 60 * time.Second
|
||||||
|
|
||||||
func DefaultConfig(modelPath string) Config {
|
func DefaultConfig(modelPath string) Config {
|
||||||
return Config{
|
return Config{
|
||||||
ModelPath: modelPath,
|
ModelPath: modelPath,
|
||||||
@@ -111,6 +124,8 @@ func DefaultConfig(modelPath string) Config {
|
|||||||
NGpuLayers: -1,
|
NGpuLayers: -1,
|
||||||
NCtx: 2048,
|
NCtx: 2048,
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
|
|
||||||
|
StartupTimeout: defaultStartupTimeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +293,10 @@ func startLlamaProc(ctx context.Context, cfg Config) (*llamaProc, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
startupTimeout := cfg.StartupTimeout
|
||||||
|
if startupTimeout <= 0 {
|
||||||
|
startupTimeout = defaultStartupTimeout
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case addr := <-portCh:
|
case addr := <-portCh:
|
||||||
p.base = addr
|
p.base = addr
|
||||||
@@ -290,10 +309,12 @@ func startLlamaProc(ctx context.Context, cfg Config) (*llamaProc, error) {
|
|||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
_ = cmd.Wait()
|
_ = cmd.Wait()
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
case <-time.After(60 * time.Second):
|
case <-time.After(startupTimeout):
|
||||||
|
// The arm most likely to leak: the child is still loading a model, so
|
||||||
|
// it is alive and busy rather than dead. Kill and reap before the error.
|
||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
_ = cmd.Wait()
|
_ = cmd.Wait()
|
||||||
return nil, fmt.Errorf("llm: server did not start within 60s")
|
return nil, fmt.Errorf("llm: server did not start within %s", startupTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,35 @@ exit 1`)
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("startup timeout", func(t *testing.T) {
|
||||||
|
// The 60s wait was a literal in the select, so this arm could only be
|
||||||
|
// tested by waiting a real minute (Vikunja #323). It is Config now.
|
||||||
|
//
|
||||||
|
// The child records its pid, because startLlamaProc returns nil on
|
||||||
|
// every failure arm and this is the arm where the child is alive and
|
||||||
|
// busy loading a model rather than already dead.
|
||||||
|
pidFile := filepath.Join(t.TempDir(), "pid")
|
||||||
|
bin := fakeLlama(t, `echo $$ > `+pidFile+`
|
||||||
|
while : ; do sleep 1 ; done`)
|
||||||
|
cfg := testCfg(bin)
|
||||||
|
cfg.StartupTimeout = 50 * time.Millisecond
|
||||||
|
_, err := startLlamaProc(context.Background(), cfg)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "did not start within") {
|
||||||
|
t.Fatalf("err = %v, want the startup-timeout arm", err)
|
||||||
|
}
|
||||||
|
raw, readErr := os.ReadFile(pidFile)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatalf("the child never ran: %v", readErr)
|
||||||
|
}
|
||||||
|
pid, convErr := strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||||
|
if convErr != nil {
|
||||||
|
t.Fatalf("pid file %q: %v", raw, convErr)
|
||||||
|
}
|
||||||
|
if err := syscall.Kill(pid, 0); err == nil {
|
||||||
|
t.Errorf("child %d survived the startup timeout", pid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("context cancelled during startup", func(t *testing.T) {
|
t.Run("context cancelled during startup", func(t *testing.T) {
|
||||||
// Never prints the listen line and never exits: only ctx can end this.
|
// Never prints the listen line and never exits: only ctx can end this.
|
||||||
bin := fakeLlama(t, `while : ; do sleep 1 ; done`)
|
bin := fakeLlama(t, `while : ; do sleep 1 ; done`)
|
||||||
|
|||||||
Reference in New Issue
Block a user