Skip to content

Commit 7469d77

Browse files
committed
feat: make llama.cpp a deferred backend on macOS/Windows
On macOS/Windows the llama.cpp server binary is now downloaded on demand into a single, writable per-user install directory (~/.docker/model-runner/llama.cpp/bin) instead of relying on a binary vendored by Docker Desktop. This removes the vendored/updated storage split and the requirement for a bundled com.docker.llama-server.digest. - Collapse vendoredServerStoragePath/updatedServerStoragePath into a single installDir; default to ~/.docker/model-runner/llama.cpp/bin when no path is configured. - Add llamacpp.NeedsDeferredInstall() (true on macOS/Windows) and mark the backend Deferred in the routing defs so it installs on first use instead of blocking startup. - Reuse main's mirror-aware dockerhub download path and the existing docker/docker-model-backend-llamacpp {version}-{variant} image tags; the download no longer hard-fails when there is no Desktop-bundled binary. - Linux keeps bundling the binary in the container image (not deferred); ensureLatestLlamaCpp remains a no-op that reports the bundled binary. - LlamaServerPath() no longer defaults to the read-only Docker Desktop bundle path; it returns the env override or empty. - Add a `docker model install-runner --backend llama.cpp` trigger for the deferred install, mirroring the vllm-metal path. Signed-off-by: Dorin Geman <dorin.geman@docker.com>
1 parent d62a635 commit 7469d77

10 files changed

Lines changed: 125 additions & 132 deletions

File tree

cmd/cli/commands/install-runner.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,18 @@ func runInstallOrStart(cmd *cobra.Command, opts runnerOptions, debug bool) error
291291
return nil
292292
}
293293

294+
// On macOS/Windows the llama.cpp backend is installed on demand: its binary
295+
// is downloaded rather than bundled at build time. Trigger installation via
296+
// the running model runner, the same way vllm-metal is handled above.
297+
if opts.backend == llamacpp.Name && llamacpp.NeedsDeferredInstall() {
298+
cmd.Println("Installing llama.cpp backend...")
299+
if err := desktopClient.InstallBackend(llamacpp.Name); err != nil {
300+
return fmt.Errorf("failed to install llama.cpp backend: %w", err)
301+
}
302+
cmd.Println("llama.cpp backend installed successfully")
303+
return nil
304+
}
305+
294306
// The diffusers backend uses deferred installation: it pulls a Docker
295307
// image, extracts a self-contained Python environment, and installs it
296308
// to a well-known local folder. Trigger installation via the running

pkg/envconfig/envconfig.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"net"
77
"os"
88
"path/filepath"
9-
"runtime"
109
"strconv"
1110
"strings"
1211

@@ -99,20 +98,13 @@ func TCPPort() string {
9998
return Var("MODEL_RUNNER_PORT")
10099
}
101100

102-
// LlamaServerPath returns the path to the llama.cpp server binary.
103-
// Configured via LLAMA_SERVER_PATH. On macOS this defaults to the Docker
104-
// Desktop bundle location as a last-resort lookup (Desktop vendors its own
105-
// copy there); on every other platform there is no meaningful default path
106-
// to check, and the daemon falls back to downloading the pinned llama.cpp
107-
// release on demand.
101+
// LlamaServerPath returns the directory that holds the llama.cpp server binary.
102+
// Configured via LLAMA_SERVER_PATH. On Linux the container image sets this to
103+
// the bundled binary's location. When it is unset (macOS/Windows), an empty
104+
// string is returned and the llama.cpp backend falls back to a writable
105+
// per-user directory into which it downloads the pinned release on demand.
108106
func LlamaServerPath() string {
109-
if s := Var("LLAMA_SERVER_PATH"); s != "" {
110-
return s
111-
}
112-
if runtime.GOOS == "darwin" {
113-
return "/Applications/Docker.app/Contents/Resources/model-runner/bin"
114-
}
115-
return ""
107+
return Var("LLAMA_SERVER_PATH")
116108
}
117109

118110
// LlamaArgs returns custom arguments to pass to the llama.cpp server.

pkg/envconfig/envconfig_test.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package envconfig_test
22

33
import (
4-
"runtime"
54
"testing"
65

76
"github.com/docker/model-runner/pkg/envconfig"
@@ -15,16 +14,13 @@ func TestLlamaServerPath(t *testing.T) {
1514
}
1615
})
1716

18-
t.Run("default depends on platform", func(t *testing.T) {
17+
t.Run("empty when unset", func(t *testing.T) {
1918
t.Setenv("LLAMA_SERVER_PATH", "")
20-
got := envconfig.LlamaServerPath()
21-
if runtime.GOOS == "darwin" {
22-
want := "/Applications/Docker.app/Contents/Resources/model-runner/bin"
23-
if got != want {
24-
t.Fatalf("LlamaServerPath() = %q, want %q", got, want)
25-
}
26-
} else if got != "" {
27-
t.Fatalf("LlamaServerPath() = %q, want empty string on %s", got, runtime.GOOS)
19+
// When unset there is no meaningful default here: the llama.cpp backend
20+
// falls back to a writable per-user directory (macOS/Windows) or the
21+
// image sets LLAMA_SERVER_PATH explicitly (Linux).
22+
if got := envconfig.LlamaServerPath(); got != "" {
23+
t.Fatalf("LlamaServerPath() = %q, want empty string", got)
2824
}
2925
})
3026
}

pkg/inference/backends/llamacpp/download.go

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func SetDesiredServerVersion(version string) {
4848

4949
//nolint:unused // Used in platform-specific files (download_darwin.go, download_windows.go)
5050
func (l *llamaCpp) downloadLatestLlamaCpp(ctx context.Context, log logging.Logger,
51-
llamaCppPath, vendoredServerStoragePath, desiredVersion, desiredVariant string,
51+
desiredVersion, desiredVariant string,
5252
) error {
5353
ShouldUpdateServerLock.Lock()
5454
shouldUpdateServer := ShouldUpdateServer
@@ -58,7 +58,10 @@ func (l *llamaCpp) downloadLatestLlamaCpp(ctx context.Context, log logging.Logge
5858
return errLlamaCppUpdateDisabled
5959
}
6060

61-
log.Info("downloadLatestLlamaCpp", "desiredVersion", desiredVersion, "desiredVariant", desiredVariant, "vendoredServerStoragePath", vendoredServerStoragePath, "llamaCppPath", llamaCppPath)
61+
llamaCppPath := filepath.Join(l.installDir, l.downloadBinaryName())
62+
versionFile := filepath.Join(l.installDir, ".llamacpp_version")
63+
64+
log.Info("downloadLatestLlamaCpp", "desiredVersion", desiredVersion, "desiredVariant", desiredVariant, "installDir", l.installDir)
6265
desiredTag := desiredVersion + "-" + desiredVariant
6366

6467
// Resolve the desired tag to a digest via the Registry HTTP API v2. This
@@ -74,30 +77,20 @@ func (l *llamaCpp) downloadLatestLlamaCpp(ctx context.Context, log logging.Logge
7477
return fmt.Errorf("could not resolve the %s tag: %w", desiredTag, err)
7578
}
7679

77-
bundledVersionFile := filepath.Join(vendoredServerStoragePath, "com.docker.llama-server.digest")
78-
currentVersionFile := filepath.Join(filepath.Dir(llamaCppPath), ".llamacpp_version")
79-
80-
data, err := os.ReadFile(bundledVersionFile)
81-
if err != nil {
82-
return fmt.Errorf("failed to read bundled llama.cpp version: %w", err)
83-
} else if strings.TrimSpace(string(data)) == latest {
84-
l.setRunningStatus(log, filepath.Join(vendoredServerStoragePath, "com.docker.llama-server"), desiredTag, latest)
85-
return errLlamaCppUpToDate
86-
}
87-
88-
data, err = os.ReadFile(currentVersionFile)
89-
if err != nil {
90-
log.Warn("failed to read current llama.cpp version", "error", err)
91-
log.Warn("proceeding to update llama.cpp binary")
92-
} else if strings.TrimSpace(string(data)) == latest {
93-
log.Info("current llama.cpp version is already up to date")
94-
if _, statErr := os.Stat(llamaCppPath); statErr == nil {
95-
l.setRunningStatus(log, llamaCppPath, desiredTag, latest)
96-
return nil
80+
// If we have already downloaded this exact digest and the binary is still
81+
// present, there is nothing to do. Unlike the previous Docker Desktop
82+
// bundled model, there is no vendored binary to compare against here.
83+
if data, readErr := os.ReadFile(versionFile); readErr == nil {
84+
if strings.TrimSpace(string(data)) == latest {
85+
if _, statErr := os.Stat(llamaCppPath); statErr == nil {
86+
log.Info("current llama.cpp version is already up to date")
87+
l.setRunningStatus(log, llamaCppPath, desiredTag, latest)
88+
return errLlamaCppUpToDate
89+
}
90+
log.Info("llama.cpp binary missing despite version match, proceeding to download")
91+
} else {
92+
log.Info("current llama.cpp version is outdated, proceeding to update", "current", strings.TrimSpace(string(data)), "latest", latest)
9793
}
98-
log.Info("llama.cpp binary must be updated, proceeding to update it")
99-
} else {
100-
log.Info("current llama.cpp version is outdated, proceeding to update it", "current", strings.TrimSpace(string(data)), "latest", latest)
10194
}
10295

10396
image := fmt.Sprintf("registry-1.docker.io/%s/%s@%s", hubNamespace, hubRepo, latest)
@@ -112,32 +105,33 @@ func (l *llamaCpp) downloadLatestLlamaCpp(ctx context.Context, log logging.Logge
112105
return fmt.Errorf("could not extract image: %w", extractErr)
113106
}
114107

115-
if err := os.RemoveAll(filepath.Dir(llamaCppPath)); err != nil && !errors.Is(err, os.ErrNotExist) {
108+
libDir := filepath.Join(filepath.Dir(l.installDir), "lib")
109+
if err := os.RemoveAll(l.installDir); err != nil && !errors.Is(err, os.ErrNotExist) {
116110
return fmt.Errorf("failed to clear inference binary dir: %w", err)
117111
}
118-
if err := os.RemoveAll(filepath.Join(filepath.Dir(filepath.Dir(llamaCppPath)), "lib")); err != nil && !errors.Is(err, os.ErrNotExist) {
112+
if err := os.RemoveAll(libDir); err != nil && !errors.Is(err, os.ErrNotExist) {
119113
return fmt.Errorf("failed to clear inference library dir: %w", err)
120114
}
121115

122-
if err := os.MkdirAll(filepath.Dir(filepath.Dir(llamaCppPath)), 0o755); err != nil {
116+
if err := os.MkdirAll(filepath.Dir(l.installDir), 0o755); err != nil {
123117
return fmt.Errorf("could not create directory for llama.cpp artifacts: %w", err)
124118
}
125119

126120
rootDir := fmt.Sprintf("com.docker.llama-server.native.%s.%s.%s", runtime.GOOS, desiredVariant, runtime.GOARCH)
127-
if err := os.Rename(filepath.Join(downloadDir, rootDir, "bin"), filepath.Dir(llamaCppPath)); err != nil {
121+
if err := os.Rename(filepath.Join(downloadDir, rootDir, "bin"), l.installDir); err != nil {
128122
return fmt.Errorf("could not move llama.cpp binary: %w", err)
129123
}
130124
if err := os.Chmod(llamaCppPath, 0o755); err != nil {
131125
return fmt.Errorf("could not chmod llama.cpp binary: %w", err)
132126
}
133127

134-
libDir := filepath.Join(downloadDir, rootDir, "lib")
135-
fi, err := os.Stat(libDir)
128+
srcLibDir := filepath.Join(downloadDir, rootDir, "lib")
129+
fi, err := os.Stat(srcLibDir)
136130
if err != nil && !errors.Is(err, os.ErrNotExist) {
137131
return fmt.Errorf("failed to stat llama.cpp lib dir: %w", err)
138132
}
139133
if err == nil && fi.IsDir() {
140-
if err := os.Rename(libDir, filepath.Join(filepath.Dir(filepath.Dir(llamaCppPath)), "lib")); err != nil {
134+
if err := os.Rename(srcLibDir, libDir); err != nil {
141135
return fmt.Errorf("could not move llama.cpp libs: %w", err)
142136
}
143137
}
@@ -146,7 +140,7 @@ func (l *llamaCpp) downloadLatestLlamaCpp(ctx context.Context, log logging.Logge
146140
l.setRunningStatus(log, llamaCppPath, desiredTag, latest)
147141
log.Info(l.status)
148142

149-
if err := os.WriteFile(currentVersionFile, []byte(latest), 0o644); err != nil {
143+
if err := os.WriteFile(versionFile, []byte(latest), 0o644); err != nil {
150144
log.Warn("failed to save llama.cpp version", "error", err)
151145
}
152146

pkg/inference/backends/llamacpp/download_darwin.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@ import (
77
"github.com/docker/model-runner/pkg/logging"
88
)
99

10-
func (l *llamaCpp) ensureLatestLlamaCpp(ctx context.Context, log logging.Logger, _ *http.Client,
11-
llamaCppPath, vendoredServerStoragePath string,
12-
) error {
10+
func (l *llamaCpp) ensureLatestLlamaCpp(ctx context.Context, log logging.Logger, _ *http.Client) error {
1311
desiredVersion := GetDesiredServerVersion()
1412
desiredVariant := "metal"
15-
return l.downloadLatestLlamaCpp(ctx, log, llamaCppPath, vendoredServerStoragePath, desiredVersion,
16-
desiredVariant)
13+
return l.downloadLatestLlamaCpp(ctx, log, desiredVersion, desiredVariant)
1714
}

pkg/inference/backends/llamacpp/download_linux.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
"github.com/docker/model-runner/pkg/logging"
99
)
1010

11-
func (l *llamaCpp) ensureLatestLlamaCpp(_ context.Context, log logging.Logger, _ *http.Client,
12-
_, vendoredServerStoragePath string,
13-
) error {
14-
l.setRunningStatus(log, filepath.Join(vendoredServerStoragePath, resolveLlamaServerBin(vendoredServerStoragePath)), "", "")
11+
func (l *llamaCpp) ensureLatestLlamaCpp(_ context.Context, log logging.Logger, _ *http.Client) error {
12+
// On Linux the binary is bundled into the container image at installDir;
13+
// there is no on-demand download.
14+
l.setRunningStatus(log, filepath.Join(l.installDir, resolveLlamaServerBin(l.installDir)), "", "")
1515
return errLlamaCppUpdateDisabled
1616
}

pkg/inference/backends/llamacpp/download_windows.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import (
1111
"github.com/docker/model-runner/pkg/logging"
1212
)
1313

14-
func (l *llamaCpp) ensureLatestLlamaCpp(ctx context.Context, log logging.Logger, _ *http.Client,
15-
llamaCppPath, vendoredServerStoragePath string,
16-
) error {
17-
nvGPUInfoBin := filepath.Join(vendoredServerStoragePath, "com.docker.nv-gpu-info.exe")
14+
func (l *llamaCpp) ensureLatestLlamaCpp(ctx context.Context, log logging.Logger, _ *http.Client) error {
15+
nvGPUInfoBin := filepath.Join(l.installDir, "com.docker.nv-gpu-info.exe")
1816
var canUseCUDA11, canUseOpenCL bool
1917
var err error
2018
ShouldUseGPUVariantLock.Lock()
@@ -43,6 +41,5 @@ func (l *llamaCpp) ensureLatestLlamaCpp(ctx context.Context, log logging.Logger,
4341
desiredVariant = "opencl"
4442
}
4543
l.status = inference.FormatInstalling(fmt.Sprintf("%s llama.cpp %s", inference.DetailCheckingForUpdates, desiredVariant))
46-
return l.downloadLatestLlamaCpp(ctx, log, llamaCppPath, vendoredServerStoragePath, desiredVersion,
47-
desiredVariant)
44+
return l.downloadLatestLlamaCpp(ctx, log, desiredVersion, desiredVariant)
4845
}

0 commit comments

Comments
 (0)