feat(backend): rfdetr-cpp native object detection + segmentation backend (#10028)

Adds a Go native gRPC backend that dlopens librfdetrcpp.so (built from
mudler/rf-detr.cpp at the pinned RFDETR_VERSION) via purego and exposes
the rfdetr.cpp inference pipeline through LocalAI's existing Detect RPC.

Supports all 5 RF-DETR detection variants (Nano/Small/Base/Medium/Large)
and 6 segmentation variants (SegNano/SegSmall/SegMedium/SegLarge/
SegXLarge/Seg2XLarge) with F32/F16/Q8_0/Q4_K quantizations. Pre-built
GGUFs ship at mudler/rfdetr-cpp-* on HuggingFace.

Detection returns Bbox + class_name + confidence; segmentation also
returns PNG-encoded per-detection masks via the rfdetr_capi accessor
functions (rfdetr_capi_get_detection_{class_id,box,score,class_name,
mask_png}).

End-to-end verified through POST /v1/detection: HTTP -> gRPC -> purego
dlopen -> rfdetr.cpp -> ggml -> response (9 detections on the detection
model, 21 detections + valid PNG masks on the seg-nano model against
the kitchen fixture).

Wiring:
  - backend/go/rfdetr-cpp/{main.go,gorfdetrcpp.go,CMakeLists.txt,
    Makefile,run.sh,package.sh,test.sh,.gitignore}
  - Top-level Makefile: BACKEND_RFDETR_CPP, docker-build target,
    .NOTPARALLEL, prepare-test-extra, test-extra
  - backend/go/rfdetr-cpp/Makefile: `test` target invoked by test-extra
  - .github/backend-matrix.yml: CPU + CUDA-12/13 + L4T CUDA-12/13
    (arm64) + HIP + Vulkan (amd64 + arm64) + SYCL f32/f16
  - backend/index.yaml: rfdetr-cpp meta anchor + latest/development
    image entries for every matrix tag-suffix
  - .github/workflows/bump_deps.yaml: RFDETR_VERSION pin tracking
    (mudler/rf-detr.cpp branch main)
  - gallery/index.yaml: 11 rfdetr-cpp-* entries (nano + 4 detection
    variants + 6 seg variants), all backed by mudler/rfdetr-cpp-*
    on HuggingFace with sha256 pinning on the F16 default
  - core/gallery/importers/rfdetr.go: GGUF auto-routing for HF imports
    (mudler/rfdetr-cpp-* repos route to rfdetr-cpp, Transformer-format
    repos stay on the Python rfdetr backend; explicit preferences.backend
    overrides both heuristics)
  - core/gallery/importers/rfdetr_test.go: table-driven coverage of the
    auto-routing + a live mudler/rfdetr-cpp-nano cross-check

scripts/changed-backends.js needs no change: the existing
Dockerfile.golang -> backend/go/${item.backend}/ branch already routes
the 9 rfdetr-cpp matrix entries to the correct backend path.

Assisted-by: Claude:claude-opus-4-7 [Claude Code]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
LocalAI [bot]
2026-05-27 18:43:57 +02:00
committed by GitHub
parent 893e69cbf8
commit 7a4ca8f60d
18 changed files with 1697 additions and 6 deletions

7
backend/go/rfdetr-cpp/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
sources/
build*/
package/
librfdetrcpp*.so
rfdetr-cpp
test-models/
test-data/

View File

@@ -0,0 +1,79 @@
cmake_minimum_required(VERSION 3.18)
project(librfdetrcpp LANGUAGES C CXX)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Static-link ggml + rfdetr so the resulting .so has no runtime dependency on
# extra ggml/rfdetr shared libraries — only on libc/libstdc++/libgomp, which
# the LocalAI package step bundles into the docker image.
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static libraries" FORCE)
# rfdetr.cpp build switches: skip CLI/tests, keep static lib.
set(RFDETR_BUILD_CLI OFF CACHE BOOL "Disable rfdetr CLI" FORCE)
set(RFDETR_BUILD_TESTS OFF CACHE BOOL "Disable rfdetr tests" FORCE)
set(RFDETR_SHARED OFF CACHE BOOL "Build rfdetr as static lib" FORCE)
# rt-detr.cpp's top-level CMakeLists invokes
# `bash ${CMAKE_SOURCE_DIR}/scripts/apply_ggml_patches.sh` to apply its
# in-tree ggml patches before descending into the submodule. When we
# `add_subdirectory` it from a parent project, `CMAKE_SOURCE_DIR` points
# at *our* directory, not theirs, so the script path resolves wrong.
#
# Run the patches script ourselves up front (it's idempotent — re-running
# is a no-op once patches are applied) so the rt-detr.cpp configure step
# is essentially a no-op for the patch hook.
set(RFDETR_CPP_SRC ${CMAKE_CURRENT_SOURCE_DIR}/sources/rt-detr.cpp)
if(EXISTS ${RFDETR_CPP_SRC}/scripts/apply_ggml_patches.sh)
execute_process(
COMMAND bash ${RFDETR_CPP_SRC}/scripts/apply_ggml_patches.sh
RESULT_VARIABLE _rfdetr_patch_result
OUTPUT_VARIABLE _rfdetr_patch_output
ERROR_VARIABLE _rfdetr_patch_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE)
if(NOT _rfdetr_patch_result EQUAL 0)
message(FATAL_ERROR
"Failed to apply ggml patches (exit ${_rfdetr_patch_result}):\n"
"stdout:\n${_rfdetr_patch_output}\n"
"stderr:\n${_rfdetr_patch_error}")
endif()
message(STATUS "${_rfdetr_patch_output}")
endif()
# Stage a shim 'scripts/apply_ggml_patches.sh' under our source dir so that
# rt-detr.cpp's CMakeLists — which calls
# bash ${CMAKE_SOURCE_DIR}/scripts/apply_ggml_patches.sh
# — finds an idempotent no-op there. The real patches have already been
# applied above; this just satisfies the path lookup.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/scripts)
file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/apply_ggml_patches.sh
"#!/usr/bin/env bash
# Shim - patches were already applied by the parent CMakeLists.
exit 0
")
execute_process(COMMAND chmod +x ${CMAKE_CURRENT_SOURCE_DIR}/scripts/apply_ggml_patches.sh)
add_subdirectory(./sources/rt-detr.cpp)
# rfdetr.cpp's C-API symbols already live inside librfdetr (src/rfdetr_capi.cpp
# is compiled into the lib). We re-export them via a MODULE library that
# whole-archive-links rfdetr so the symbols are visible at dlopen time.
add_library(rfdetrcpp MODULE
sources/rt-detr.cpp/src/rfdetr_capi.cpp)
target_include_directories(rfdetrcpp PRIVATE
sources/rt-detr.cpp/include
sources/rt-detr.cpp/src
sources/rt-detr.cpp/third_party/stb
)
target_link_libraries(rfdetrcpp PRIVATE rfdetr ggml)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0)
target_link_libraries(rfdetrcpp PRIVATE stdc++fs)
endif()
set_property(TARGET rfdetrcpp PROPERTY CXX_STANDARD 17)
set_target_properties(rfdetrcpp PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

View File

@@ -0,0 +1,135 @@
CMAKE_ARGS?=
BUILD_TYPE?=
NATIVE?=false
GOCMD?=go
GO_TAGS?=
JOBS?=$(shell nproc --ignore=1)
# rt-detr.cpp (GitHub redirects the historical mudler/rt-detr.cpp to the new
# mudler/rf-detr.cpp slug). Pin to a specific commit if you need a stable
# build; leaving this on `master` always picks up the latest C-API surface
# (incl. the per-detection accessor functions used by gorfdetrcpp.go).
RFDETR_REPO?=https://github.com/mudler/rf-detr.cpp.git
RFDETR_VERSION?=main
ifeq ($(NATIVE),false)
CMAKE_ARGS+=-DGGML_NATIVE=OFF
endif
# Forward LocalAI's BUILD_TYPE to the matching ggml backend switch.
ifeq ($(BUILD_TYPE),cublas)
CMAKE_ARGS+=-DGGML_CUDA=ON -DRFDETR_GGML_CUDA=ON
else ifeq ($(BUILD_TYPE),openblas)
CMAKE_ARGS+=-DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS
else ifeq ($(BUILD_TYPE),clblas)
CMAKE_ARGS+=-DGGML_CLBLAST=ON
else ifeq ($(BUILD_TYPE),hipblas)
ROCM_HOME ?= /opt/rocm
ROCM_PATH ?= /opt/rocm
export CXX=$(ROCM_HOME)/llvm/bin/clang++
export CC=$(ROCM_HOME)/llvm/bin/clang
AMDGPU_TARGETS?=gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1200,gfx1201
CMAKE_ARGS+=-DGGML_HIPBLAS=ON -DRFDETR_GGML_HIPBLAS=ON -DAMDGPU_TARGETS=$(AMDGPU_TARGETS)
else ifeq ($(BUILD_TYPE),vulkan)
CMAKE_ARGS+=-DGGML_VULKAN=ON -DRFDETR_GGML_VULKAN=ON
else ifeq ($(OS),Darwin)
ifneq ($(BUILD_TYPE),metal)
CMAKE_ARGS+=-DGGML_METAL=OFF
else
CMAKE_ARGS+=-DGGML_METAL=ON
CMAKE_ARGS+=-DGGML_METAL_EMBED_LIBRARY=ON
CMAKE_ARGS+=-DRFDETR_GGML_METAL=ON
endif
endif
ifeq ($(BUILD_TYPE),sycl_f16)
CMAKE_ARGS+=-DGGML_SYCL=ON \
-DCMAKE_C_COMPILER=icx \
-DCMAKE_CXX_COMPILER=icpx \
-DGGML_SYCL_F16=ON
endif
ifeq ($(BUILD_TYPE),sycl_f32)
CMAKE_ARGS+=-DGGML_SYCL=ON \
-DCMAKE_C_COMPILER=icx \
-DCMAKE_CXX_COMPILER=icpx
endif
sources/rt-detr.cpp:
mkdir -p sources && \
git clone --recursive $(RFDETR_REPO) sources/rt-detr.cpp && \
cd sources/rt-detr.cpp && \
git checkout $(RFDETR_VERSION) && \
git submodule update --init --recursive --depth 1 --single-branch
# Detect OS
UNAME_S := $(shell uname -s)
# Only build CPU variants on Linux
ifeq ($(UNAME_S),Linux)
VARIANT_TARGETS = librfdetrcpp-avx.so librfdetrcpp-avx2.so librfdetrcpp-avx512.so librfdetrcpp-fallback.so
else
# On non-Linux (e.g., Darwin), build only fallback variant
VARIANT_TARGETS = librfdetrcpp-fallback.so
endif
rfdetr-cpp: main.go gorfdetrcpp.go $(VARIANT_TARGETS)
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o rfdetr-cpp ./
package: rfdetr-cpp
bash package.sh
build: package
clean: purge
rm -rf librfdetrcpp*.so rfdetr-cpp package sources
purge:
rm -rf build*
# Build all variants (Linux only)
ifeq ($(UNAME_S),Linux)
librfdetrcpp-avx.so: sources/rt-detr.cpp
rm -rfv build-$@
$(info ${GREEN}I rfdetr-cpp build info:avx${RESET})
SO_TARGET=$@ CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) librfdetrcpp-custom
rm -rfv build-$@
librfdetrcpp-avx2.so: sources/rt-detr.cpp
rm -rfv build-$@
$(info ${GREEN}I rfdetr-cpp build info:avx2${RESET})
SO_TARGET=$@ CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=off -DGGML_FMA=on -DGGML_F16C=on -DGGML_BMI2=on" $(MAKE) librfdetrcpp-custom
rm -rfv build-$@
librfdetrcpp-avx512.so: sources/rt-detr.cpp
rm -rfv build-$@
$(info ${GREEN}I rfdetr-cpp build info:avx512${RESET})
SO_TARGET=$@ CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=on -DGGML_FMA=on -DGGML_F16C=on -DGGML_BMI2=on" $(MAKE) librfdetrcpp-custom
rm -rfv build-$@
endif
# Build fallback variant (all platforms)
librfdetrcpp-fallback.so: sources/rt-detr.cpp
rm -rfv build-$@
$(info ${GREEN}I rfdetr-cpp build info:fallback${RESET})
SO_TARGET=$@ CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) librfdetrcpp-custom
rm -rfv build-$@
librfdetrcpp-custom: CMakeLists.txt
mkdir -p build-$(SO_TARGET) && \
cd build-$(SO_TARGET) && \
cmake .. $(CMAKE_ARGS) && \
cmake --build . --config Release -j$(JOBS) && \
cd .. && \
mv build-$(SO_TARGET)/librfdetrcpp.so ./$(SO_TARGET)
all: rfdetr-cpp package
# `test` is invoked by the top-level Makefile's `test-extra` target. It builds
# the backend binary + the fallback shared library (needed for dlopen at
# runtime), then runs test.sh which downloads the test models + COCO image
# and exercises the gRPC Load/Detect wire path via the Go smoke test in
# main_test.go for both the detection and segmentation models.
test: rfdetr-cpp librfdetrcpp-fallback.so
bash test.sh

View File

@@ -0,0 +1,195 @@
package main
// gorfdetrcpp.go - gRPC handlers (Load, Detect) for the rfdetr-cpp backend.
//
// Embeds base.SingleThread to default unimplemented RPCs to "not supported"
// while we only implement object detection.
import (
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strconv"
"unsafe"
"github.com/mudler/LocalAI/pkg/grpc/base"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
)
// Default upper bound on detections returned per image. RF-DETR's decoder
// queries are limited to a few hundred; 300 is a safe ceiling.
const defaultTopK = 300
// rfdetr_handle_t is a uintptr-typed opaque handle (see include/rfdetr_capi.h).
var (
// rfdetr_capi_load(const char* model_path, int n_threads, rfdetr_handle_t* out_handle) -> int
CapiLoad func(modelPath string, nThreads int32, outHandle *uintptr) int32
// rfdetr_capi_unload(rfdetr_handle_t handle) -> int
CapiUnload func(handle uintptr) int32
// rfdetr_capi_detect_path(handle, image_path, threshold, top_k, out_json) -> int
CapiDetectPath func(handle uintptr, imagePath string, threshold float32, topK uint32, outJSON *uintptr) int32
// rfdetr_capi_detect_buffer(handle, bytes, len, threshold, top_k, out_json) -> int
CapiDetectBuffer func(handle uintptr, bytes uintptr, length uintptr, threshold float32, topK uint32, outJSON *uintptr) int32
// rfdetr_capi_free_string(char* s)
CapiFreeString func(s uintptr)
// rfdetr_capi_get_n_detections(handle) -> int
CapiGetNDetections func(handle uintptr) int32
// rfdetr_capi_get_detection_class_id(handle, i) -> int
CapiGetDetectionClassID func(handle uintptr, i int32) int32
// rfdetr_capi_get_detection_box(handle, i, out_xyxy[4]) -> int (0 on success)
CapiGetDetectionBox func(handle uintptr, i int32, outXYXY uintptr) int32
// rfdetr_capi_get_detection_score(handle, i) -> float
CapiGetDetectionScore func(handle uintptr, i int32) float32
// rfdetr_capi_get_detection_class_name(handle, i, buf, buf_size) -> int (needed/written; two-call sizing)
CapiGetDetectionClassName func(handle uintptr, i int32, buf uintptr, bufSize int32) int32
// rfdetr_capi_get_detection_mask_png(handle, i, buf, buf_size) -> int (needed/written; 0 means no mask)
CapiGetDetectionMaskPNG func(handle uintptr, i int32, buf uintptr, bufSize int32) int32
)
type RFDetrCpp struct {
base.SingleThread
handle uintptr
}
// Load loads the GGUF model at opts.ModelFile (joined with opts.ModelPath if relative)
// and stores the handle for later Detect calls.
func (r *RFDetrCpp) Load(opts *pb.ModelOptions) error {
modelFile := opts.ModelFile
if modelFile == "" {
modelFile = opts.Model
}
if modelFile == "" {
return fmt.Errorf("rfdetr-cpp: ModelFile is empty")
}
var modelPath string
if filepath.IsAbs(modelFile) {
modelPath = modelFile
} else {
modelPath = filepath.Join(opts.ModelPath, modelFile)
}
if _, err := os.Stat(modelPath); err != nil {
return fmt.Errorf("rfdetr-cpp: model file not found: %s: %w", modelPath, err)
}
threads := opts.Threads
if threads <= 0 {
threads = 4
}
// Release previous model if any (re-Load).
if r.handle != 0 {
CapiUnload(r.handle)
r.handle = 0
}
var h uintptr
rc := CapiLoad(modelPath, threads, &h)
if rc != 0 || h == 0 {
return fmt.Errorf("rfdetr-cpp: rfdetr_capi_load failed with rc=%d for %s", rc, modelPath)
}
r.handle = h
return nil
}
// Detect runs object detection on the base64-encoded image in opts.Src at
// opts.Threshold, returning one pb.Detection per result. Seg models also
// populate Detection.Mask with PNG-encoded mask bytes.
func (r *RFDetrCpp) Detect(opts *pb.DetectOptions) (pb.DetectResponse, error) {
if r.handle == 0 {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: model not loaded")
}
// Decode base64 image and write to temp file.
imgData, err := base64.StdEncoding.DecodeString(opts.Src)
if err != nil {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to decode base64 image: %w", err)
}
tmpFile, err := os.CreateTemp("", "rfdetr-*.img")
if err != nil {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to create temp file: %w", err)
}
defer func() { _ = os.Remove(tmpFile.Name()) }()
if _, err := tmpFile.Write(imgData); err != nil {
_ = tmpFile.Close()
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to write temp file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: failed to close temp file: %w", err)
}
threshold := opts.Threshold
if threshold <= 0 {
threshold = 0.5
}
// JSON output from detect_path is unused: we read structured detections via
// the accessor functions. Still must free the returned string.
var jsonPtr uintptr
rc := CapiDetectPath(r.handle, tmpFile.Name(), threshold, uint32(defaultTopK), &jsonPtr)
if jsonPtr != 0 {
CapiFreeString(jsonPtr)
}
if rc != 0 {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: detect failed with rc=%d", rc)
}
n := CapiGetNDetections(r.handle)
if n < 0 {
return pb.DetectResponse{}, fmt.Errorf("rfdetr-cpp: invalid n_detections=%d", n)
}
detections := make([]*pb.Detection, 0, n)
for i := int32(0); i < n; i++ {
var bbox [4]float32 // x1, y1, x2, y2
if rc := CapiGetDetectionBox(r.handle, i, uintptr(unsafe.Pointer(&bbox[0]))); rc != 0 {
continue
}
cid := CapiGetDetectionClassID(r.handle, i)
score := CapiGetDetectionScore(r.handle, i)
// Two-call sizing for class_name.
var className string
nameSize := CapiGetDetectionClassName(r.handle, i, 0, 0)
if nameSize > 1 {
buf := make([]byte, nameSize)
written := CapiGetDetectionClassName(r.handle, i, uintptr(unsafe.Pointer(&buf[0])), nameSize)
// `written` is the same number (needed bytes including NUL); strip NUL.
if written > 0 && int(written) <= len(buf) {
className = string(buf[:written-1])
} else {
className = string(buf[:len(buf)-1])
}
}
if className == "" {
className = strconv.Itoa(int(cid))
}
// Two-call sizing for mask PNG (returns 0 when no mask).
var mask []byte
maskSize := CapiGetDetectionMaskPNG(r.handle, i, 0, 0)
if maskSize > 0 {
maskBuf := make([]byte, maskSize)
CapiGetDetectionMaskPNG(r.handle, i, uintptr(unsafe.Pointer(&maskBuf[0])), maskSize)
mask = maskBuf
}
detections = append(detections, &pb.Detection{
X: bbox[0],
Y: bbox[1],
Width: bbox[2] - bbox[0],
Height: bbox[3] - bbox[1],
Confidence: score,
ClassName: className,
Mask: mask,
})
}
return pb.DetectResponse{
Detections: detections,
}, nil
}

View File

@@ -0,0 +1,61 @@
package main
// main.go - entry point for the rfdetr-cpp gRPC backend.
//
// Dlopens librfdetrcpp-<variant>.so via purego at the path in
// RFDETR_LIBRARY (set by run.sh based on /proc/cpuinfo), registers the
// rfdetr_capi_* C ABI symbols, then starts the gRPC server.
import (
"flag"
"os"
"github.com/ebitengine/purego"
grpc "github.com/mudler/LocalAI/pkg/grpc"
)
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
)
type LibFuncs struct {
FuncPtr any
Name string
}
func main() {
// Get library name from environment variable, default to fallback
libName := os.Getenv("RFDETR_LIBRARY")
if libName == "" {
libName = "./librfdetrcpp-fallback.so"
}
rfdetrLib, err := purego.Dlopen(libName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
panic(err)
}
libFuncs := []LibFuncs{
{&CapiLoad, "rfdetr_capi_load"},
{&CapiUnload, "rfdetr_capi_unload"},
{&CapiDetectPath, "rfdetr_capi_detect_path"},
{&CapiDetectBuffer, "rfdetr_capi_detect_buffer"},
{&CapiFreeString, "rfdetr_capi_free_string"},
{&CapiGetNDetections, "rfdetr_capi_get_n_detections"},
{&CapiGetDetectionClassID, "rfdetr_capi_get_detection_class_id"},
{&CapiGetDetectionBox, "rfdetr_capi_get_detection_box"},
{&CapiGetDetectionScore, "rfdetr_capi_get_detection_score"},
{&CapiGetDetectionClassName, "rfdetr_capi_get_detection_class_name"},
{&CapiGetDetectionMaskPNG, "rfdetr_capi_get_detection_mask_png"},
}
for _, lf := range libFuncs {
purego.RegisterLibFunc(lf.FuncPtr, rfdetrLib, lf.Name)
}
flag.Parse()
if err := grpc.StartServer(*addr, &RFDetrCpp{}); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,220 @@
package main
// main_test.go - end-to-end smoke test for the rfdetr-cpp gRPC backend.
//
// Spawns the compiled rfdetr-cpp binary on a free local port, dials it via
// gRPC, and exercises LoadModel + Detect against the test fixtures
// downloaded by test.sh. Two scenarios:
//
// 1. detection — loads rfdetr-nano-q8_0.gguf and asserts at least one
// detection comes back with a non-empty class name and a bounding box
// of non-zero size.
// 2. segmentation — loads rfdetr-seg-nano-q8_0.gguf and additionally
// asserts that at least one detection carries a PNG-encoded mask blob
// (verified by PNG magic bytes).
//
// Both specs Skip cleanly if their fixtures are missing so the test target
// stays usable on a fresh checkout where models haven't been downloaded.
import (
"context"
"encoding/base64"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func TestRFDetrCpp(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "rfdetr-cpp backend smoke suite")
}
// freePort grabs an ephemeral TCP port and immediately releases it so the
// spawned backend can bind to it. There is a tiny TOCTOU window here but in
// practice it's adequate for a smoke test on a quiet runner.
func freePort() int {
l, err := net.Listen("tcp", "127.0.0.1:0")
Expect(err).ToNot(HaveOccurred(), "freePort listen")
port := l.Addr().(*net.TCPAddr).Port
Expect(l.Close()).To(Succeed())
return port
}
// startBackend spawns the rfdetr-cpp binary on the given port and waits
// until it accepts TCP connections (up to 10s). The returned cleanup func
// kills the process and reaps it.
func startBackend(port int) func() {
binary, err := filepath.Abs("./rfdetr-cpp")
Expect(err).ToNot(HaveOccurred())
if _, err := os.Stat(binary); err != nil {
Skip(fmt.Sprintf("backend binary not built: %s (run `make rfdetr-cpp` first)", binary))
}
libPath, err := filepath.Abs("./librfdetrcpp-fallback.so")
Expect(err).ToNot(HaveOccurred())
if _, err := os.Stat(libPath); err != nil {
Skip(fmt.Sprintf("fallback library not built: %s (run `make librfdetrcpp-fallback.so` first)", libPath))
}
addr := fmt.Sprintf("127.0.0.1:%d", port)
cmd := exec.Command(binary, "--addr", addr)
cmd.Env = append(os.Environ(), "RFDETR_LIBRARY="+libPath)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
Expect(cmd.Start()).To(Succeed())
cleanup := func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
_, _ = cmd.Process.Wait()
}
}
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
c, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
if err == nil {
_ = c.Close()
return cleanup
}
time.Sleep(200 * time.Millisecond)
}
cleanup()
Fail(fmt.Sprintf("backend did not become ready on %s within 10s", addr))
return func() {}
}
// loadTestImage reads the COCO test image downloaded by test.sh and returns
// its base64-encoded content (the wire format accepted by the Detect RPC).
func loadTestImage() string {
imgPath, err := filepath.Abs("test-data/test.jpg")
Expect(err).ToNot(HaveOccurred())
imgBytes, err := os.ReadFile(imgPath)
if err != nil {
Skip(fmt.Sprintf("test image not present: %s (run test.sh first)", imgPath))
}
return base64.StdEncoding.EncodeToString(imgBytes)
}
// dialBackend opens a gRPC client connection to the spawned backend.
func dialBackend(port int) (pb.BackendClient, func()) {
addr := fmt.Sprintf("127.0.0.1:%d", port)
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
Expect(err).ToNot(HaveOccurred())
return pb.NewBackendClient(conn), func() { _ = conn.Close() }
}
// modelPathOrSkip resolves a model file under ./test-models/ and Skip()s
// the current spec if it's missing.
func modelPathOrSkip(name string) string {
modelDir, err := filepath.Abs("test-models")
Expect(err).ToNot(HaveOccurred())
modelPath := filepath.Join(modelDir, name)
if _, err := os.Stat(modelPath); err != nil {
Skip(fmt.Sprintf("model not present: %s (run test.sh first)", modelPath))
}
return modelPath
}
var _ = Describe("rfdetr-cpp backend", func() {
It("runs object detection against a known-good COCO image", func() {
modelPath := modelPathOrSkip("rfdetr-nano-q8_0.gguf")
imgB64 := loadTestImage()
port := freePort()
cleanup := startBackend(port)
defer cleanup()
client, closeConn := dialBackend(port)
defer closeConn()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
loadResp, err := client.LoadModel(ctx, &pb.ModelOptions{
Model: "rfdetr-nano-q8_0.gguf",
ModelFile: modelPath,
Threads: 2,
})
Expect(err).ToNot(HaveOccurred(), "LoadModel")
Expect(loadResp.GetSuccess()).To(BeTrue(), "LoadModel reported failure: %s", loadResp.GetMessage())
detResp, err := client.Detect(ctx, &pb.DetectOptions{
Src: imgB64,
Threshold: 0.5,
})
Expect(err).ToNot(HaveOccurred(), "Detect")
Expect(detResp.GetDetections()).ToNot(BeEmpty(), "no detections returned on a known-good COCO image")
_, _ = fmt.Fprintf(GinkgoWriter, "detection OK: %d detections\n", len(detResp.GetDetections()))
for i, d := range detResp.GetDetections() {
Expect(d.GetClassName()).ToNot(BeEmpty(), "detection %d has empty class_name", i)
Expect(d.GetConfidence()).To(BeNumerically(">=", float32(0.5)),
"detection %d below threshold", i)
Expect(d.GetWidth()).To(BeNumerically(">", float32(0)),
"detection %d has non-positive width", i)
Expect(d.GetHeight()).To(BeNumerically(">", float32(0)),
"detection %d has non-positive height", i)
}
})
It("runs segmentation and returns PNG-encoded masks", func() {
modelPath := modelPathOrSkip("rfdetr-seg-nano-q8_0.gguf")
imgB64 := loadTestImage()
port := freePort()
cleanup := startBackend(port)
defer cleanup()
client, closeConn := dialBackend(port)
defer closeConn()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
loadResp, err := client.LoadModel(ctx, &pb.ModelOptions{
Model: "rfdetr-seg-nano-q8_0.gguf",
ModelFile: modelPath,
Threads: 2,
})
Expect(err).ToNot(HaveOccurred(), "LoadModel")
Expect(loadResp.GetSuccess()).To(BeTrue(), "LoadModel reported failure: %s", loadResp.GetMessage())
detResp, err := client.Detect(ctx, &pb.DetectOptions{
Src: imgB64,
Threshold: 0.5,
})
Expect(err).ToNot(HaveOccurred(), "Detect")
Expect(detResp.GetDetections()).ToNot(BeEmpty(), "no detections returned from segmentation model")
haveMask := false
for i, d := range detResp.GetDetections() {
m := d.GetMask()
if len(m) == 0 {
continue
}
haveMask = true
// Verify PNG magic: 89 50 4E 47 ("\x89PNG").
Expect(len(m)).To(BeNumerically(">=", 4), "detection %d mask too short", i)
Expect([]byte{m[0], m[1], m[2], m[3]}).To(Equal([]byte{0x89, 'P', 'N', 'G'}),
"detection %d mask is not a PNG", i)
}
Expect(haveMask).To(BeTrue(),
"segmentation model returned %d detections but none carried a mask",
len(detResp.GetDetections()))
_, _ = fmt.Fprintf(GinkgoWriter, "segmentation OK: %d detections, at least one with PNG mask\n",
len(detResp.GetDetections()))
})
})

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Script to copy the appropriate libraries based on architecture
set -e
CURDIR=$(dirname "$(realpath $0)")
REPO_ROOT="${CURDIR}/../../.."
# Create lib directory
mkdir -p $CURDIR/package/lib
cp -avf $CURDIR/librfdetrcpp-*.so $CURDIR/package/
cp -avf $CURDIR/rfdetr-cpp $CURDIR/package/
cp -fv $CURDIR/run.sh $CURDIR/package/
# Detect architecture and copy appropriate libraries
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
# x86_64 architecture
echo "Detected x86_64 architecture, copying x86_64 libraries..."
cp -arfLv /lib64/ld-linux-x86-64.so.2 $CURDIR/package/lib/ld.so
cp -arfLv /lib/x86_64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
cp -arfLv /lib/x86_64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
cp -arfLv /lib/x86_64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
cp -arfLv /lib/x86_64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
cp -arfLv /lib/x86_64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
cp -arfLv /lib/x86_64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
cp -arfLv /lib/x86_64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
cp -arfLv /lib/x86_64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
# ARM64 architecture
echo "Detected ARM64 architecture, copying ARM64 libraries..."
cp -arfLv /lib/ld-linux-aarch64.so.1 $CURDIR/package/lib/ld.so
cp -arfLv /lib/aarch64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
cp -arfLv /lib/aarch64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
cp -arfLv /lib/aarch64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
cp -arfLv /lib/aarch64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
cp -arfLv /lib/aarch64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
cp -arfLv /lib/aarch64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
cp -arfLv /lib/aarch64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
cp -arfLv /lib/aarch64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
elif [ $(uname -s) = "Darwin" ]; then
echo "Detected Darwin"
else
echo "Error: Could not detect architecture"
exit 1
fi
# Package GPU libraries based on BUILD_TYPE
GPU_LIB_SCRIPT="${REPO_ROOT}/scripts/build/package-gpu-libs.sh"
if [ -f "$GPU_LIB_SCRIPT" ]; then
echo "Packaging GPU libraries for BUILD_TYPE=${BUILD_TYPE:-cpu}..."
source "$GPU_LIB_SCRIPT" "$CURDIR/package/lib"
package_gpu_libs
fi
echo "Packaging completed successfully"
ls -liah $CURDIR/package/
ls -liah $CURDIR/package/lib/

52
backend/go/rfdetr-cpp/run.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
set -ex
# Get the absolute current dir where the script is located
CURDIR=$(dirname "$(realpath $0)")
cd /
echo "CPU info:"
if [ "$(uname)" != "Darwin" ]; then
grep -e "model\sname" /proc/cpuinfo | head -1
grep -e "flags" /proc/cpuinfo | head -1
fi
LIBRARY="$CURDIR/librfdetrcpp-fallback.so"
if [ "$(uname)" != "Darwin" ]; then
if grep -q -e "\savx\s" /proc/cpuinfo ; then
echo "CPU: AVX found OK"
if [ -e $CURDIR/librfdetrcpp-avx.so ]; then
LIBRARY="$CURDIR/librfdetrcpp-avx.so"
fi
fi
if grep -q -e "\savx2\s" /proc/cpuinfo ; then
echo "CPU: AVX2 found OK"
if [ -e $CURDIR/librfdetrcpp-avx2.so ]; then
LIBRARY="$CURDIR/librfdetrcpp-avx2.so"
fi
fi
# Check avx 512
if grep -q -e "\savx512f\s" /proc/cpuinfo ; then
echo "CPU: AVX512F found OK"
if [ -e $CURDIR/librfdetrcpp-avx512.so ]; then
LIBRARY="$CURDIR/librfdetrcpp-avx512.so"
fi
fi
fi
export LD_LIBRARY_PATH=$CURDIR/lib:$LD_LIBRARY_PATH
export RFDETR_LIBRARY=$LIBRARY
# If there is a lib/ld.so, use it
if [ -f $CURDIR/lib/ld.so ]; then
echo "Using lib/ld.so"
echo "Using library: $LIBRARY"
exec $CURDIR/lib/ld.so $CURDIR/rfdetr-cpp "$@"
fi
echo "Using library: $LIBRARY"
exec $CURDIR/rfdetr-cpp "$@"

55
backend/go/rfdetr-cpp/test.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
set -e
CURDIR=$(dirname "$(realpath $0)")
echo "Running rfdetr-cpp backend tests..."
# Test models from the mudler/rfdetr-cpp-* HuggingFace repos. Both the
# detection (nano-q8_0, ~36 MB) and segmentation (seg-nano-q8_0, ~40 MB)
# variants are downloaded so the Go smoke test exercises both code paths.
RFDETR_MODEL_DIR="${RFDETR_MODEL_DIR:-$CURDIR/test-models}"
RFDETR_DET_FILE="${RFDETR_DET_FILE:-rfdetr-nano-q8_0.gguf}"
RFDETR_DET_URL="${RFDETR_DET_URL:-https://huggingface.co/mudler/rfdetr-cpp-nano/resolve/main/rfdetr-nano-q8_0.gguf}"
RFDETR_SEG_FILE="${RFDETR_SEG_FILE:-rfdetr-seg-nano-q8_0.gguf}"
RFDETR_SEG_URL="${RFDETR_SEG_URL:-https://huggingface.co/mudler/rfdetr-cpp-seg-nano/resolve/main/rfdetr-seg-nano-q8_0.gguf}"
mkdir -p "$RFDETR_MODEL_DIR"
if [ ! -f "$RFDETR_MODEL_DIR/$RFDETR_DET_FILE" ]; then
echo "Downloading rfdetr nano-q8_0 detection model..."
curl -L -o "$RFDETR_MODEL_DIR/$RFDETR_DET_FILE" "$RFDETR_DET_URL" --progress-bar
fi
if [ ! -f "$RFDETR_MODEL_DIR/$RFDETR_SEG_FILE" ]; then
echo "Downloading rfdetr seg-nano-q8_0 segmentation model..."
curl -L -o "$RFDETR_MODEL_DIR/$RFDETR_SEG_FILE" "$RFDETR_SEG_URL" --progress-bar
fi
# Use a real COCO test image from the upstream rf-detr.cpp repo (~46 KB).
# A synthetic 64x64 red PNG was too synthetic to elicit detections from a
# real model — the smoke test would always trivially pass with zero
# detections.
TEST_IMAGE_DIR="$CURDIR/test-data"
TEST_IMAGE_FILE="$TEST_IMAGE_DIR/test.jpg"
TEST_IMAGE_URL="${TEST_IMAGE_URL:-https://raw.githubusercontent.com/mudler/rf-detr.cpp/main/tests/fixtures/ci/test_image.jpg}"
mkdir -p "$TEST_IMAGE_DIR"
if [ ! -f "$TEST_IMAGE_FILE" ]; then
echo "Downloading COCO test image..."
curl -L -o "$TEST_IMAGE_FILE" "$TEST_IMAGE_URL" --progress-bar
fi
echo "rfdetr-cpp test setup complete."
echo " detection model: $RFDETR_MODEL_DIR/$RFDETR_DET_FILE"
echo " segmentation model: $RFDETR_MODEL_DIR/$RFDETR_SEG_FILE"
echo " test image: $TEST_IMAGE_FILE"
# Run the Go smoke test: spawns the backend binary on a free port, calls
# LoadModel + Detect via gRPC for both detection and segmentation models.
echo ""
echo "Running Go smoke test..."
cd "$CURDIR"
go test -v -timeout 5m ./...

View File

@@ -253,6 +253,34 @@
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-sam3-cpp"
intel: "intel-sycl-f32-sam3-cpp"
vulkan: "vulkan-sam3-cpp"
- &rfdetrcpp
name: "rfdetr-cpp"
alias: "rfdetr-cpp"
license: apache-2.0
description: |
Native RF-DETR object detection and instance segmentation in C/C++
using GGML. Loads pre-built GGUF weights from the mudler/rfdetr-cpp-*
family (Nano/Small/Base/Medium/Large + SegNano/SegSmall/SegMedium)
and returns bounding boxes, class labels, confidence scores, and
(for segmentation variants) PNG-encoded per-detection masks.
urls:
- https://github.com/mudler/rf-detr.cpp
tags:
- object-detection
- image-segmentation
- rfdetr
- gpu
- cpu
capabilities:
default: "cpu-rfdetr-cpp"
nvidia: "cuda12-rfdetr-cpp"
nvidia-cuda-12: "cuda12-rfdetr-cpp"
nvidia-cuda-13: "cuda13-rfdetr-cpp"
nvidia-l4t: "nvidia-l4t-arm64-rfdetr-cpp"
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-rfdetr-cpp"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-rfdetr-cpp"
intel: "intel-sycl-f32-rfdetr-cpp"
vulkan: "vulkan-rfdetr-cpp"
- &vllm
name: "vllm"
license: apache-2.0
@@ -2349,6 +2377,99 @@
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-vulkan-sam3-cpp"
mirrors:
- localai/localai-backends:master-gpu-vulkan-sam3-cpp
## rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "rfdetr-cpp-development"
capabilities:
default: "cpu-rfdetr-cpp-development"
nvidia: "cuda12-rfdetr-cpp-development"
nvidia-cuda-12: "cuda12-rfdetr-cpp-development"
nvidia-cuda-13: "cuda13-rfdetr-cpp-development"
nvidia-l4t: "nvidia-l4t-arm64-rfdetr-cpp-development"
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-rfdetr-cpp-development"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-rfdetr-cpp-development"
intel: "intel-sycl-f32-rfdetr-cpp-development"
vulkan: "vulkan-rfdetr-cpp-development"
- !!merge <<: *rfdetrcpp
name: "cpu-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-cpu-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cpu-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-cpu-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cuda12-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-12-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cuda12-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-12-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cuda13-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-13-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-13-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cuda13-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-13-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "nvidia-l4t-arm64-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-arm64-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-nvidia-l4t-arm64-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "nvidia-l4t-arm64-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-arm64-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-nvidia-l4t-arm64-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cuda13-nvidia-l4t-arm64-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-cuda-13-arm64-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-nvidia-l4t-cuda-13-arm64-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "cuda13-nvidia-l4t-arm64-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-cuda-13-arm64-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-nvidia-l4t-cuda-13-arm64-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "intel-sycl-f32-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-intel-sycl-f32-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-gpu-intel-sycl-f32-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "intel-sycl-f32-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-intel-sycl-f32-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-gpu-intel-sycl-f32-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "intel-sycl-f16-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-intel-sycl-f16-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-gpu-intel-sycl-f16-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "intel-sycl-f16-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-intel-sycl-f16-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-gpu-intel-sycl-f16-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "vulkan-rfdetr-cpp"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-vulkan-rfdetr-cpp"
mirrors:
- localai/localai-backends:latest-gpu-vulkan-rfdetr-cpp
- !!merge <<: *rfdetrcpp
name: "vulkan-rfdetr-cpp-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-vulkan-rfdetr-cpp"
mirrors:
- localai/localai-backends:master-gpu-vulkan-rfdetr-cpp
## Rerankers
- !!merge <<: *rerankers
name: "rerankers-development"