The mirror would happily hold a four-shard model that nothing could serve. A catalog entry names one source, the agent fetched one URL, and so a version of several files had no way to be described — which is every safetensors model, and every GGUF too large to ship whole. A source ending in a slash names a version rather than a file. The agent fetches every file in it into one directory and hands the engine the directory: vLLM reads the shards and the config out of the folder, and llama.cpp is given the first part and finds the rest beside it. The trailing slash is the whole signal. A flag beside the source would be a second place for the two to disagree, and a URL already distinguishes a directory from a file everywhere else. The mirror answers a version with a manifest instead of a file listing. A plain file server gives HTML there, and something that has to know what a version consists of would be left parsing it. It is on the byte listener, not the management API: fetching weights is the one thing every node does, and making it depend on the administrative port would undo the split that keeps that port closeable. Built from the directory rather than a file written beside it. A manifest on disk is a second record of what is there, and the two disagree the first time somebody copies a file in by hand — which the mirror explicitly allows. The whole set or nothing. A directory with three of four shards loads as a broken model rather than failing to load, so a completion marker is written last and an interrupted fetch starts the missing files again rather than being found and served. Files already there at the right size are not fetched twice. Progress is reported under the version's address, not each file's: the shards waiting on it asked for the version, and progress under a file's own URL reaches nobody. A filename from the manifest is refused if it contains a separator. The mirror is a place an operator copies files into by hand, so what it reports is not automatically safe to join onto a path. An HTML listing where a manifest was expected says so. "invalid character '<'" sends somebody looking at the wrong thing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
324 lines
10 KiB
Go
324 lines
10 KiB
Go
// Package weights makes a model's files present on a node.
|
|
//
|
|
// The simple arrangement, and the default: every node has its own copy. A
|
|
// source that is already a local path is used as it stands; one that names a
|
|
// remote file is fetched into a cache directory once and reused. Nothing here
|
|
// manages storage, decides a retention policy, or knows what a filesystem is —
|
|
// it answers one question, which is where the engine should be pointed.
|
|
//
|
|
// The exotic arrangements are all handled by not acting. Weights on a shared
|
|
// filer, an image exported over the fabric, or anything else an operator has
|
|
// mounted are simply present, and present means there is nothing to do. That is
|
|
// why the interesting case needs no code: the escape hatch from a distribution
|
|
// mechanism is a distribution mechanism that checks first.
|
|
package weights
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Progress reports how a fetch is going, for anything that wants to say so.
|
|
type Progress struct {
|
|
Source string
|
|
BytesDone int64
|
|
BytesTotal int64
|
|
}
|
|
|
|
// Percent is how far along, or -1 when the total is unknown.
|
|
func (p Progress) Percent() int {
|
|
if p.BytesTotal <= 0 {
|
|
return -1
|
|
}
|
|
return int(p.BytesDone * 100 / p.BytesTotal)
|
|
}
|
|
|
|
// Fetcher makes sources local.
|
|
type Fetcher struct {
|
|
CacheDir string
|
|
Client *http.Client
|
|
Log *slog.Logger
|
|
// OnProgress is called occasionally during a fetch. A missing model is the
|
|
// difference between a replica that is slow to start and one that is stuck,
|
|
// and without this the two are indistinguishable from outside.
|
|
OnProgress func(Progress)
|
|
|
|
// inflight makes concurrent requests for one source share a single fetch.
|
|
//
|
|
// Eight shards of a tensor-parallel replica start at the same instant and
|
|
// want the same file. Without this they would each open a connection and
|
|
// each write the same bytes, turning one download into eight and, on a
|
|
// shared origin, into a self-inflicted denial of service.
|
|
mu sync.Mutex
|
|
inflight map[string]*fetchCall
|
|
}
|
|
|
|
type fetchCall struct {
|
|
done chan struct{}
|
|
path string
|
|
err error
|
|
}
|
|
|
|
// ErrNotLocal means a source names a file that is not here and cannot be
|
|
// fetched — a bare path with nothing to fetch it from.
|
|
var ErrNotLocal = errors.New("the weights are not on this node and the source says nothing about where to get them")
|
|
|
|
// Ensure returns a local path for a source, fetching it if it is not here.
|
|
//
|
|
// A plain path is never fetched, only checked. That is deliberate: a path is
|
|
// what an operator writes when the file is already there, whether it arrived on
|
|
// local disk, a Lustre mount or an image exported over the fabric, and a
|
|
// downloader that second-guessed that would copy a hundred gigabytes that were
|
|
// already present.
|
|
func (f *Fetcher) Ensure(ctx context.Context, source string) (string, error) {
|
|
if source == "" {
|
|
return "", fmt.Errorf("no source given")
|
|
}
|
|
// A version rather than a file. Everything below assumes one URL is one
|
|
// file, and a set has to be handled before any of it.
|
|
if IsSet(source) {
|
|
return f.ensureSet(ctx, source)
|
|
}
|
|
if !isRemote(source) {
|
|
if !isPath(source) {
|
|
// A bare model id — "Qwen/Qwen2.5-7B-Instruct" — is not a file and
|
|
// was never meant to be one. vLLM and SGLang resolve those
|
|
// themselves, and doing it for them would be both a duplicate
|
|
// downloader and a change in documented behaviour.
|
|
return source, nil
|
|
}
|
|
if _, err := os.Stat(source); err == nil {
|
|
return source, nil
|
|
}
|
|
return "", fmt.Errorf("%w: %s", ErrNotLocal, source)
|
|
}
|
|
|
|
target, err := f.cachePath(source)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := os.Stat(target); err == nil {
|
|
return target, nil
|
|
}
|
|
|
|
f.mu.Lock()
|
|
if f.inflight == nil {
|
|
f.inflight = map[string]*fetchCall{}
|
|
}
|
|
if call, running := f.inflight[source]; running {
|
|
f.mu.Unlock()
|
|
select {
|
|
case <-call.done:
|
|
return call.path, call.err
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
}
|
|
}
|
|
call := &fetchCall{done: make(chan struct{})}
|
|
f.inflight[source] = call
|
|
f.mu.Unlock()
|
|
|
|
call.path, call.err = f.fetch(ctx, source, target)
|
|
close(call.done)
|
|
|
|
f.mu.Lock()
|
|
delete(f.inflight, source)
|
|
f.mu.Unlock()
|
|
return call.path, call.err
|
|
}
|
|
|
|
// fetch downloads a source to a target, resuming and verifying.
|
|
func (f *Fetcher) fetch(ctx context.Context, source, target string) (string, error) {
|
|
return f.fetchAs(ctx, source, source, target)
|
|
}
|
|
|
|
// fetchAs downloads one URL but reports progress under another name.
|
|
//
|
|
// The two differ for a set: the shards waiting on it asked for the version, not
|
|
// for the fourth file in it, and progress reported under the file's own URL
|
|
// reaches nobody. What they see is one number climbing across the whole set.
|
|
func (f *Fetcher) fetchAs(ctx context.Context, source, reportAs, target string) (string, error) {
|
|
url, err := resolveURL(source)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Written beside the target and renamed at the end. A partial file under
|
|
// the real name would be found by the next scan, reported as present, and
|
|
// handed to an engine that fails on it minutes later — the exact failure
|
|
// this whole area exists to prevent.
|
|
part := target + ".part"
|
|
existing := int64(0)
|
|
if st, err := os.Stat(part); err == nil {
|
|
existing = st.Size()
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if existing > 0 {
|
|
// Resume. A forty gigabyte fetch that fails at ninety percent must not
|
|
// start again from nothing.
|
|
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", existing))
|
|
}
|
|
|
|
client := f.Client
|
|
if client == nil {
|
|
client = &http.Client{}
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("fetching %s: %w", source, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
existing = 0 // the server ignored the range; start over
|
|
case http.StatusPartialContent:
|
|
default:
|
|
return "", fmt.Errorf("fetching %s: %s returned %s", source, url, resp.Status)
|
|
}
|
|
|
|
flags := os.O_CREATE | os.O_WRONLY
|
|
if existing > 0 {
|
|
flags |= os.O_APPEND
|
|
} else {
|
|
flags |= os.O_TRUNC
|
|
}
|
|
out, err := os.OpenFile(part, flags, 0o644)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
total := existing + resp.ContentLength
|
|
f.report(Progress{Source: reportAs, BytesDone: existing, BytesTotal: total})
|
|
written, err := f.copyWithProgress(ctx, out, resp.Body, reportAs, existing, total)
|
|
closeErr := out.Close()
|
|
if err != nil {
|
|
// The part file is kept on purpose, so the next attempt resumes.
|
|
return "", fmt.Errorf("fetching %s after %d bytes: %w", source, written, err)
|
|
}
|
|
if closeErr != nil {
|
|
return "", closeErr
|
|
}
|
|
|
|
if err := os.Rename(part, target); err != nil {
|
|
return "", err
|
|
}
|
|
if f.Log != nil {
|
|
f.Log.Info("weights fetched", "source", source, "path", target, "bytes", written)
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func (f *Fetcher) copyWithProgress(ctx context.Context, dst io.Writer, src io.Reader,
|
|
source string, done, total int64,
|
|
) (int64, error) {
|
|
buf := make([]byte, 4<<20)
|
|
last := time.Now()
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return done, err
|
|
}
|
|
n, err := src.Read(buf)
|
|
if n > 0 {
|
|
if _, werr := dst.Write(buf[:n]); werr != nil {
|
|
return done, werr
|
|
}
|
|
done += int64(n)
|
|
// Occasionally, not per read: a four-megabyte buffer over a
|
|
// hundred gigabytes is twenty-five thousand callbacks, and nobody
|
|
// needs to be told that often.
|
|
if time.Since(last) > 2*time.Second {
|
|
f.report(Progress{Source: source, BytesDone: done, BytesTotal: total})
|
|
last = time.Now()
|
|
}
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
f.report(Progress{Source: source, BytesDone: done, BytesTotal: total})
|
|
return done, nil
|
|
}
|
|
if err != nil {
|
|
return done, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) report(p Progress) {
|
|
if f.OnProgress != nil {
|
|
f.OnProgress(p)
|
|
}
|
|
}
|
|
|
|
// cachePath is where a remote source lands.
|
|
//
|
|
// Named by a hash of the source with the original filename kept on the end, so
|
|
// two models with the same filename from different places cannot collide while
|
|
// a human looking at the directory can still tell what is in it.
|
|
func (f *Fetcher) cachePath(source string) (string, error) {
|
|
if f.CacheDir == "" {
|
|
return "", fmt.Errorf("no cache directory configured, so %s cannot be fetched", source)
|
|
}
|
|
return CachePathFor(f.CacheDir, source), nil
|
|
}
|
|
|
|
// isPath reports whether a source names a file on this machine.
|
|
//
|
|
// By its leading character, because that is the only honest signal available:
|
|
// "Qwen/Qwen2.5-7B-Instruct" and "models/qwen.gguf" are indistinguishable as
|
|
// strings, and an operator writing a local file writes an absolute path. The
|
|
// cost of being wrong in this direction is that a relative path is passed to
|
|
// the engine untouched, which is what happened before this code existed.
|
|
func isPath(source string) bool {
|
|
return strings.HasPrefix(source, "/") ||
|
|
strings.HasPrefix(source, "./") ||
|
|
strings.HasPrefix(source, "../")
|
|
}
|
|
|
|
// isRemote reports whether a source has to be fetched at all.
|
|
func isRemote(source string) bool {
|
|
return strings.HasPrefix(source, "hf:") ||
|
|
strings.HasPrefix(source, "http://") ||
|
|
strings.HasPrefix(source, "https://")
|
|
}
|
|
|
|
// resolveURL turns a source into something to GET.
|
|
//
|
|
// hf: names one file in a repository, which is what a GGUF is. A whole
|
|
// repository of shards is deliberately not handled here: vLLM and SGLang
|
|
// download those themselves and do it well, and reimplementing a snapshot
|
|
// downloader to compete with them would be work spent to arrive back where we
|
|
// started.
|
|
func resolveURL(source string) (string, error) {
|
|
if rest, ok := strings.CutPrefix(source, "hf:"); ok {
|
|
parts := strings.Split(rest, "/")
|
|
if len(parts) < 3 {
|
|
return "", fmt.Errorf(
|
|
"%s names a repository, not a file; give the file as hf:org/repo/model.gguf, "+
|
|
"or use the repository id directly and let the engine download it", source)
|
|
}
|
|
repo := strings.Join(parts[:2], "/")
|
|
file := strings.Join(parts[2:], "/")
|
|
return "https://huggingface.co/" + repo + "/resolve/main/" + file, nil
|
|
}
|
|
if _, err := url.Parse(source); err != nil {
|
|
return "", fmt.Errorf("%s is not a usable source: %w", source, err)
|
|
}
|
|
return source, nil
|
|
}
|