Storage will be different at every installation — Lustre, NFS over RDMA, an image exported with SRP or NVMe-oF, local NVMe, an rsync — so llmgw observes it rather than managing it. An agent told which directories to look in reports what it can see; how the files got there is nobody's business here, and a node told nothing reports nothing and behaves exactly as every node did before this existed. That fact buys two things. Placement prefers a node that already holds the model over one that must first move tens of gigabytes, as a preference and never a filter: fetching them is a valid answer, and refusing to place would turn a slow start into no start at all. And ctl status shows what each fabric holds. Per fabric because fabrics are not connected to one another. Weights in ib-a are not weights in ib-b — the only path between them is the management network — so a model seeded into one fabric leaves the rest of the cluster unable to serve it without a slow copy, and nothing said so before now. One shared filer counts the same as twenty local copies, because "can this fabric serve it" is the question being asked. Three distinctions the tests hold to, because each is a way to get this quietly wrong. A node that was never asked to look must rank with one that looked and has nothing, or enabling the feature on a single node would drain every other. An empty directory is not weights, because a half-finished download looks exactly like one and would send placement to a node whose engine then fails to load. And local storage is never mistaken for shared: a filer read as local costs an unnecessary copy, while local read as shared would have the cluster believe a model is everywhere when it is on one node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
4.3 KiB
Go
138 lines
4.3 KiB
Go
package discovery
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// LocalWeights is one model file or directory a node can see.
|
|
type LocalWeights struct {
|
|
Path string `json:"path"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
// Shared marks weights on a filesystem this node did not have to fill.
|
|
//
|
|
// Worth distinguishing: twenty nodes each holding their own copy and twenty
|
|
// nodes seeing one filer are the same list of paths and completely
|
|
// different situations — one has twenty copies of a 141 GB model and the
|
|
// other has one.
|
|
Shared bool `json:"shared,omitempty"`
|
|
}
|
|
|
|
// ScanWeights lists the model files under the given directories.
|
|
//
|
|
// Storage is the part of this system that will differ at every installation —
|
|
// a Lustre mount, an image exported over the fabric, local NVMe, an rsync from
|
|
// a laptop — so llmgw does not try to own it. It observes the result instead:
|
|
// whatever put the file there, the question placement needs answered is only
|
|
// whether it is present.
|
|
//
|
|
// Scanning nothing is the default and constrains nothing. A node with no
|
|
// directories configured behaves exactly as every node did before this existed,
|
|
// which is what makes the whole feature optional.
|
|
func ScanWeights(dirs []string) []LocalWeights {
|
|
var out []LocalWeights
|
|
seen := map[string]struct{}{}
|
|
|
|
for _, dir := range dirs {
|
|
if dir == "" {
|
|
continue
|
|
}
|
|
shared := isNetworkFS(dir)
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
// A directory that is not there is not an error to report upward:
|
|
// a mount that has not come back yet looks exactly like this, and
|
|
// the node is simply one without those weights until it does.
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
path := filepath.Join(dir, e.Name())
|
|
if _, dup := seen[path]; dup {
|
|
continue
|
|
}
|
|
size, ok := weightSize(path, e)
|
|
if !ok {
|
|
continue
|
|
}
|
|
seen[path] = struct{}{}
|
|
out = append(out, LocalWeights{Path: path, SizeBytes: size, Shared: shared})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
|
|
return out
|
|
}
|
|
|
|
// weightSize measures a candidate and says whether it is one at all.
|
|
//
|
|
// A model is either a single file — a .gguf, usually — or a directory of
|
|
// shards and a config, which is what every HuggingFace checkout looks like.
|
|
// Anything else in the directory is somebody's notes and is not reported.
|
|
func weightSize(path string, e fs.DirEntry) (int64, bool) {
|
|
if e.IsDir() {
|
|
// A directory counts only if it actually holds weights. An empty one
|
|
// is a half-finished download, and reporting it as present would have
|
|
// placement choose a node whose engine then fails to load.
|
|
var total int64
|
|
var found bool
|
|
_ = filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil //nolint:nilerr // an unreadable subtree is not fatal
|
|
}
|
|
if isWeightFile(p) {
|
|
found = true
|
|
}
|
|
if info, err := d.Info(); err == nil {
|
|
total += info.Size()
|
|
}
|
|
return nil
|
|
})
|
|
return total, found
|
|
}
|
|
if !isWeightFile(path) {
|
|
return 0, false
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return info.Size(), true
|
|
}
|
|
|
|
func isWeightFile(path string) bool {
|
|
switch strings.ToLower(filepath.Ext(path)) {
|
|
case ".gguf", ".safetensors", ".bin", ".pt", ".pth":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isNetworkFS reports whether a path is on a filesystem the node did not fill.
|
|
//
|
|
// By magic number rather than by parsing mounts, because the mount table says
|
|
// what was asked for and this says what the kernel actually has. The list is
|
|
// the network filesystems that turn up in front of GPUs: NFS, Lustre, GPFS,
|
|
// CephFS, GlusterFS and SMB. Anything unrecognised is treated as local, which
|
|
// is the conservative direction — a shared filer mistaken for local storage
|
|
// costs an unnecessary copy, while local storage mistaken for shared would have
|
|
// the cluster believe a model is everywhere when it is on one node.
|
|
func isNetworkFS(path string) bool {
|
|
var st syscall.Statfs_t
|
|
if err := syscall.Statfs(path, &st); err != nil {
|
|
return false
|
|
}
|
|
switch uint32(st.Type) { //nolint:unconvert // Type is int64 on some platforms
|
|
case 0x6969, // NFS
|
|
0x0BD00BD0, // Lustre
|
|
0x47504653, // GPFS
|
|
0x00C36400, // CephFS
|
|
0x65735546, // FUSE, which is how GlusterFS and many object mounts appear
|
|
0xFF534D42: // SMB/CIFS
|
|
return true
|
|
}
|
|
return false
|
|
}
|