Skip to content

Commit 4c6750f

Browse files
localai-botmudler
andauthored
feat(depth): metric-large + nested metric model gallery entries (#10363)
* feat(depth): add depth-anything-3-metric-large gallery entry DA3METRIC-LARGE (ViT-L) single-file metric-scale depth + sky, served by the existing depth-anything backend (same single-GGUF path as mono-large). GGUF published at mudler/depth-anything.cpp-gguf. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(depth): serve nested metric model (two-file load) The DA3 nested model needs both branches (anyview GIANT + metric ViT-L) loaded together. Wire it through the backend: - Load reads a 'metric_model:<file>' entry from ModelOptions.Options and, when present, calls da_capi_load_nested(anyview, metric) instead of da_capi_load (registers the new abi-4 symbol; helper optionValue + unit test). - gallery: depth-anything-3-nested (model=anyview, options=metric branch, both GGUFs fetched) for metric-scale depth + pose. - bump depth-anything.cpp pin to cce5edc (abi 4 / da_capi_load_nested). Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent a6e1c6d commit 4c6750f

6 files changed

Lines changed: 193 additions & 14 deletions

File tree

backend/go/depth-anything-cpp/Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ JOBS?=$(shell nproc --ignore=1)
88

99
# depth-anything.cpp. Pin to a specific commit for a stable build; a squash
1010
# merge upstream can orphan a branch, so the native version is pinned by SHA.
11-
# The SHA is kept alive by the v0.1.2 tag on the upstream repo.
11+
# This SHA adds the nested two-file metric C-API (abi_version 4,
12+
# da_capi_load_nested) required by the depth-anything-3-nested gallery model;
13+
# tag it (e.g. v0.1.3) upstream to keep the SHA alive.
1214
DEPTHANYTHING_REPO?=https://github.com/mudler/depth-anything.cpp.git
13-
DEPTHANYTHING_VERSION?=442eea4f73e83ca9d9bc8e026b966cffa678ffc4
15+
DEPTHANYTHING_VERSION?=cce5edc395fd1843806093d7ccc0c8b0d0b97b72
1416

1517
ifeq ($(NATIVE),false)
1618
CMAKE_ARGS+=-DGGML_NATIVE=OFF

backend/go/depth-anything-cpp/godepthanythingcpp.go

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"math"
2525
"os"
2626
"path/filepath"
27+
"strings"
2728
"unsafe"
2829

2930
"github.com/mudler/LocalAI/pkg/grpc/base"
@@ -36,6 +37,10 @@ import (
3637
var (
3738
// da_capi_load(const char* gguf_path, int n_threads) -> da_ctx* (0 = fail)
3839
CapiLoad func(gguf string, nThreads int32) uintptr
40+
// da_capi_load_nested(const char* anyview_gguf, const char* metric_gguf,
41+
// int n_threads) -> da_ctx* (0 = fail). The returned ctx serves the nested
42+
// metric model: depth/pose calls produce final metric-scale depth + scaled pose.
43+
CapiLoadNested func(anyview string, metric string, nThreads int32) uintptr
3944
// da_capi_free(da_ctx* ctx) — safe on a 0 handle.
4045
CapiFree func(handle uintptr)
4146
// da_capi_last_error(da_ctx* ctx) -> const char* (owned by ctx, "" if none).
@@ -87,17 +92,24 @@ func (r *DepthAnythingCpp) Load(opts *pb.ModelOptions) error {
8792
return fmt.Errorf("depth-anything-cpp: ModelFile is empty")
8893
}
8994

90-
var modelPath string
91-
if filepath.IsAbs(modelFile) {
92-
modelPath = modelFile
93-
} else {
94-
modelPath = filepath.Join(opts.ModelPath, modelFile)
95+
resolve := func(name string) string {
96+
if filepath.IsAbs(name) {
97+
return name
98+
}
99+
return filepath.Join(opts.ModelPath, name)
95100
}
101+
modelPath := resolve(modelFile)
96102

97103
if _, err := os.Stat(modelPath); err != nil {
98104
return fmt.Errorf("depth-anything-cpp: model file not found: %s: %w", modelPath, err)
99105
}
100106

107+
// Nested metric models are a two-file pair: the main model is the anyview
108+
// (GIANT) branch and the metric (ViT-L + DPT/sky) branch is named via a
109+
// "metric_model:<filename>" entry in opts.Options. When present we load both
110+
// branches so the engine runs the nested metric alignment.
111+
metricFile := optionValue(opts.Options, "metric_model")
112+
101113
threads := opts.Threads
102114
if threads <= 0 {
103115
threads = 4
@@ -109,19 +121,47 @@ func (r *DepthAnythingCpp) Load(opts *pb.ModelOptions) error {
109121
r.handle = 0
110122
}
111123

112-
h := CapiLoad(modelPath, threads)
113-
if h == 0 {
114-
// da_capi_last_error needs a ctx; on a failed load we have none (it
115-
// returns "" for a null ctx), so the text is best-effort.
116-
if msg := CapiLastError(0); msg != "" {
117-
return fmt.Errorf("depth-anything-cpp: da_capi_load failed for %s: %s", modelPath, msg)
124+
var h uintptr
125+
if metricFile != "" {
126+
metricPath := resolve(metricFile)
127+
if _, err := os.Stat(metricPath); err != nil {
128+
return fmt.Errorf("depth-anything-cpp: metric_model file not found: %s: %w", metricPath, err)
129+
}
130+
h = CapiLoadNested(modelPath, metricPath, threads)
131+
if h == 0 {
132+
if msg := CapiLastError(0); msg != "" {
133+
return fmt.Errorf("depth-anything-cpp: da_capi_load_nested failed for %s + %s: %s", modelPath, metricPath, msg)
134+
}
135+
return fmt.Errorf("depth-anything-cpp: da_capi_load_nested failed for %s + %s", modelPath, metricPath)
136+
}
137+
} else {
138+
h = CapiLoad(modelPath, threads)
139+
if h == 0 {
140+
// da_capi_last_error needs a ctx; on a failed load we have none (it
141+
// returns "" for a null ctx), so the text is best-effort.
142+
if msg := CapiLastError(0); msg != "" {
143+
return fmt.Errorf("depth-anything-cpp: da_capi_load failed for %s: %s", modelPath, msg)
144+
}
145+
return fmt.Errorf("depth-anything-cpp: da_capi_load failed for %s", modelPath)
118146
}
119-
return fmt.Errorf("depth-anything-cpp: da_capi_load failed for %s", modelPath)
120147
}
121148
r.handle = h
122149
return nil
123150
}
124151

152+
// optionValue returns the value of the first "key:value" entry in opts whose key
153+
// matches (case-sensitive), or "" if absent. Mirrors how other LocalAI backends
154+
// read ModelOptions.Options.
155+
func optionValue(opts []string, key string) string {
156+
prefix := key + ":"
157+
for _, o := range opts {
158+
if strings.HasPrefix(o, prefix) {
159+
return strings.TrimSpace(o[len(prefix):])
160+
}
161+
}
162+
return ""
163+
}
164+
125165
// depthResult is the JSON payload returned by Predict.
126166
type depthResult struct {
127167
DepthW int `json:"depth_w"`
@@ -373,6 +413,10 @@ func copyBytes(p *byte, n int) []byte {
373413
// runDepthPose runs depth estimation then pose recovery on an image file. It
374414
// returns the row-major depth map (length h*w), its dimensions, the 3x4
375415
// extrinsics (12 floats) and 3x3 intrinsics (9 floats).
416+
// runDepthPose returns depth + camera pose via two C-API calls (depth then pose).
417+
// For a nested metric model both calls run the full two-branch pipeline, so this
418+
// path infers twice; the typed Depth RPC (single da_capi_depth_dense call) is the
419+
// efficient path for nested models.
376420
func (r *DepthAnythingCpp) runDepthPose(imagePath string) (depth []float32, h, w int, ext [12]float32, intr [9]float32, err error) {
377421
if r.handle == 0 {
378422
err = fmt.Errorf("depth-anything-cpp: model not loaded")

backend/go/depth-anything-cpp/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func main() {
3737

3838
libFuncs := []LibFuncs{
3939
{&CapiLoad, "da_capi_load"},
40+
{&CapiLoadNested, "da_capi_load_nested"},
4041
{&CapiFree, "da_capi_free"},
4142
{&CapiLastError, "da_capi_last_error"},
4243
{&CapiDepthPath, "da_capi_depth_path"},
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
// nested_e2e_test.go - e2e smoke for the nested two-file metric model. Loads the
4+
// anyview branch as the main model and points the metric branch via the
5+
// "metric_model:<file>" option (exactly as the depth-anything-3-nested gallery
6+
// entry does), then exercises the typed Depth RPC and asserts a metric depth map.
7+
//
8+
// Skips cleanly unless both nested GGUFs are present under ./test-models/ and the
9+
// backend binary + fallback .so are built.
10+
11+
import (
12+
"context"
13+
"fmt"
14+
"path/filepath"
15+
"time"
16+
17+
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
18+
. "github.com/onsi/ginkgo/v2"
19+
. "github.com/onsi/gomega"
20+
)
21+
22+
var _ = Describe("depth-anything-cpp nested metric model", func() {
23+
It("loads the two-file pair via the metric_model option and returns metric depth", func() {
24+
anyviewPath := modelPathOrSkip("depth-anything-nested-anyview.gguf")
25+
_ = modelPathOrSkip("depth-anything-nested-metric.gguf")
26+
imgB64 := loadTestImage()
27+
28+
port := freePort()
29+
cleanup := startBackend(port)
30+
defer cleanup()
31+
32+
client, closeConn := dialBackend(port)
33+
defer closeConn()
34+
35+
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Minute)
36+
defer cancel()
37+
38+
loadResp, err := client.LoadModel(ctx, &pb.ModelOptions{
39+
Model: "depth-anything-nested-anyview.gguf",
40+
ModelFile: anyviewPath,
41+
ModelPath: filepath.Dir(anyviewPath),
42+
Options: []string{"metric_model:depth-anything-nested-metric.gguf"},
43+
Threads: 8,
44+
})
45+
Expect(err).ToNot(HaveOccurred(), "LoadModel(nested)")
46+
Expect(loadResp.GetSuccess()).To(BeTrue(), "LoadModel reported failure: %s", loadResp.GetMessage())
47+
48+
resp, err := client.Depth(ctx, &pb.DepthRequest{
49+
Src: imgB64,
50+
IncludeDepth: true,
51+
IncludePose: true,
52+
})
53+
Expect(err).ToNot(HaveOccurred(), "Depth(nested)")
54+
Expect(resp.GetWidth()).To(BeNumerically(">", 0), "depth width")
55+
Expect(resp.GetHeight()).To(BeNumerically(">", 0), "depth height")
56+
Expect(resp.GetIsMetric()).To(BeTrue(), "nested output must be metric")
57+
Expect(len(resp.GetDepth())).To(Equal(int(resp.GetWidth())*int(resp.GetHeight())), "dense depth length")
58+
Expect(len(resp.GetExtrinsics())).To(Equal(12), "extrinsics 3x4")
59+
Expect(resp.GetIntrinsics()[0]).To(BeNumerically(">", 0), "fx > 0")
60+
61+
_, _ = fmt.Fprintf(GinkgoWriter, "nested depth OK: %dx%d is_metric=%v fx=%.2f\n",
62+
resp.GetWidth(), resp.GetHeight(), resp.GetIsMetric(), resp.GetIntrinsics()[0])
63+
})
64+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = DescribeTable("optionValue",
9+
func(opts []string, key, want string) {
10+
Expect(optionValue(opts, key)).To(Equal(want))
11+
},
12+
Entry("present", []string{"foo:bar", "metric_model:m.gguf"}, "metric_model", "m.gguf"),
13+
Entry("absent", []string{"foo:bar"}, "metric_model", ""),
14+
Entry("nil", []string(nil), "metric_model", ""),
15+
Entry("trims space", []string{"metric_model: m.gguf "}, "metric_model", "m.gguf"),
16+
Entry("value with colon", []string{"metric_model:a:b.gguf"}, "metric_model", "a:b.gguf"),
17+
Entry("first wins", []string{"metric_model:first.gguf", "metric_model:second.gguf"}, "metric_model", "first.gguf"),
18+
Entry("empty value", []string{"metric_model:"}, "metric_model", ""),
19+
Entry("prefix not key", []string{"metric_model_extra:x"}, "metric_model", ""),
20+
)

gallery/index.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8162,6 +8162,54 @@
81628162
- filename: depth-anything-mono-large-f32.gguf
81638163
uri: huggingface://mudler/depth-anything.cpp-gguf/depth-anything-mono-large-f32.gguf
81648164
sha256: "291b1a554af907c3f79986ee225da8933be5f7a31d73c81d06784cda284535de"
8165+
8166+
- !!merge <<: *depth-anything-3-base
8167+
name: depth-anything-3-metric-large
8168+
description: |
8169+
Depth Anything 3 (metric large / vitl), f32 (~1.3 GB) — single-image
8170+
metric-scale depth (meters) + a sky mask. DPT single-head metric variant; use
8171+
GenerateImage (src -> normalized depth PNG) or Predict (JSON metric depth
8172+
stats, is_metric=true).
8173+
overrides:
8174+
backend: depth-anything
8175+
parameters:
8176+
model: depth-anything-metric-large-f32.gguf
8177+
files:
8178+
- filename: depth-anything-metric-large-f32.gguf
8179+
uri: huggingface://mudler/depth-anything.cpp-gguf/depth-anything-metric-large-f32.gguf
8180+
sha256: "d10b7450c2238244b2d72e2749537a1876255180149cd630a18bc1619c9286be"
8181+
8182+
- !!merge <<: *depth-anything-3-base
8183+
name: depth-anything-3-nested
8184+
description: |
8185+
Depth Anything 3 (nested giant+large), f32 — the recommended metric model. A
8186+
two-branch pipeline: the anyview GIANT (vitg) branch and a metric ViT-L branch
8187+
are run and aligned to recover true metric-scale depth (meters) + scaled camera
8188+
pose from a single image. Downloads both branches (~6 GB total); GPU strongly
8189+
recommended. Predict returns metric depth stats + pose (is_metric=true).
8190+
tags:
8191+
- depth-estimation
8192+
- camera-pose
8193+
- metric-depth
8194+
- depth-anything
8195+
- native
8196+
- cpp
8197+
- gpu
8198+
overrides:
8199+
backend: depth-anything
8200+
# The metric (ViT-L) branch is loaded alongside the anyview model via the
8201+
# metric_model option; both files are fetched below.
8202+
options:
8203+
- "metric_model:depth-anything-nested-metric.gguf"
8204+
parameters:
8205+
model: depth-anything-nested-anyview.gguf
8206+
files:
8207+
- filename: depth-anything-nested-anyview.gguf
8208+
uri: huggingface://mudler/depth-anything.cpp-gguf/depth-anything-nested-anyview.gguf
8209+
sha256: "2a4cb4382aa8c4159fff10dfffa121f3c7a574551c4ff4ad130f235d5442f9ce"
8210+
- filename: depth-anything-nested-metric.gguf
8211+
uri: huggingface://mudler/depth-anything.cpp-gguf/depth-anything-nested-metric.gguf
8212+
sha256: "b54ed50cbc0b0c14fae1f8edd0fea8bd1cac0850485fd6e7eb2422c7a19e570e"
81658213
- name: rfdetr-cpp-base
81668214
url: github:mudler/LocalAI/gallery/virtual.yaml@master
81678215
urls:

0 commit comments

Comments
 (0)