Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions catalog/models/glm-4.7-flash.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ variants:
default_config:
quantization: int4
n_gpu_layers: 999
ctx_size: 8192
# Full trained context (202752). Deploy auto-clamps it down to fit hardware
# memory on smaller machines (weights ~18GB + KV cache), so this large default
# is safe everywhere.
ctx_size: 202752
expected_performance:
startup_time_s: 20
cold_start_time_s: 40
tokens_per_second: [48, 60]
latency_first_token_ms: [30, 200]
notes: "Verified 2026-06-08 on AMD Strix Halo Radeon 8060S iGPU, llama.cpp b9180 HIP, Q4_K_M (deepseek2 arch, 30B-A3B MoE) all 999 layers offloaded: 58.9 tok/s decode, 83 tok/s prompt."
notes: "Verified 2026-06-08 on AMD Strix Halo Radeon 8060S iGPU, llama.cpp b9180 HIP, Q4_K_M (deepseek2 arch, 30B-A3B MoE) all 999 layers offloaded: 58.9 tok/s decode, 83 tok/s prompt. Context verified 2026-06-16: llama-server loads & serves n_ctx=202752 (full trained context) on the iGPU."
# --- GB10 (Blackwell, 128GB unified, K3S) ---
# BF16 ~59GB, 128GB unified memory 可容纳
# Glm4MoeLiteForCausalLM 架构 (glm4_moe_lite model_type)
Expand Down
13 changes: 11 additions & 2 deletions catalog/models/qwen2.5-vl-3b-instruct.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ variants:
hardware:
gpu_arch: "*"
vram_min_mib: 0
ram_min_mib: 4096
ram_min_mib: 8192
engine: llamacpp
format: gguf
default_config:
quantization: int4
n_gpu_layers: 999
ctx_size: 8192
# Context window = the model's full trained context, 128000 (128K) — verified
# on Strix Halo: llama-server loads & serves n_ctx=128000 (KV cache ~36 KB/token
# → ~5 GB at 128K; model Q4 ~2 GB; so ~8 GB total, hence ram_min 8192). 8192 was
# too small for agent clients (OpenClaw injects all MCP tool schemas → base prompt
# ~9.3K tokens > 8192 → "context overflow", model never replies). A large n_ctx
# only reserves KV memory up front; it does NOT slow short conversations (llama
# processes only the actual tokens). Do NOT exceed 128000 (the trained limit) or
# llama-server warns of training-context overflow. Override per-deploy with
# `--config ctx_size=N` on memory-constrained hardware.
ctx_size: 128000
expected_performance:
startup_time_s: 10
cold_start_time_s: 20
Expand Down
51 changes: 51 additions & 0 deletions catalog/models/qwen3-embedding-4b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
kind: model_asset
metadata:
name: qwen3-embedding-4b
type: embedding
family: qwen
parameter_count: "4B"
released_at: "2025-06"
# Scan-name aliases: on-disk GGUF names so the local scanner matches this asset.
aliases:
- Qwen3-Embedding-4B
- Qwen3-Embedding-4B-Q4_K_M
- Qwen3-Embedding-4B-Q8_0
- qwen3-embedding-4b-q4_k_m
- qwen3-embedding-4b-q8_0
storage:
formats: [safetensors, gguf]
default_path_pattern: "{{.DataDir}}/models/{{.Name}}"
sources:
- type: huggingface
repo: Qwen/Qwen3-Embedding-4B
format: safetensors
- type: huggingface
repo: Qwen/Qwen3-Embedding-4B-GGUF
format: gguf
- type: modelscope
repo: Qwen/Qwen3-Embedding-4B
format: safetensors
- type: local_path
path: ""
variants:
# --- universal llamacpp GGUF (verified on AMD Strix Halo Radeon 8060S iGPU) ---
# Text-embedding model: deploy llama-server in --embedding mode (config embedding=true
# → --embedding) and serve OpenAI /v1/embeddings. 2560-dim output. Not a chat model,
# so it is NOT written into OpenClaw config by `aima openclaw sync` (embedding models
# have no chat provider mapping); use the /v1/embeddings endpoint directly.
- name: qwen3-embedding-4b-universal-llamacpp
hardware:
gpu_arch: "*"
vram_min_mib: 0
ram_min_mib: 6144
engine: llamacpp
format: gguf
default_config:
n_gpu_layers: 999
embedding: true
# Native context (40960). Deploy auto-clamps to fit hardware memory if needed.
ctx_size: 40960
expected_performance:
startup_time_s: 8
cold_start_time_s: 15
notes: "Verified 2026-06-16 on AMD Strix Halo Radeon 8060S iGPU, llama.cpp b9180 HIP, Q8_0 all layers offloaded: deploys in --embedding mode (config embedding=true), /v1/embeddings returns 2560-dim vectors. ctx_size 40960."
28 changes: 27 additions & 1 deletion catalog/models/qwen3.6-35b-a3b.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,42 @@ metadata:
released_at: "2026-04"
aliases:
- Qwen3.6-35B-A3B
- Qwen3.6-35B-A3B-UD-Q4_K_M
- qwen3.6-35b-a3b-q4_k_m
storage:
formats: [safetensors]
formats: [safetensors, gguf]
default_path_pattern: "{{.DataDir}}/models/{{.Name}}"
sources:
- type: huggingface
repo: Qwen/Qwen3.6-35B-A3B
format: safetensors
- type: huggingface
repo: unsloth/Qwen3.6-35B-A3B-GGUF
format: gguf
- type: local_path
path: ""
variants:
# --- universal llamacpp GGUF (verified on AMD Strix Halo Radeon 8060S iGPU) ---
# Qwen3.6-35B-A3B MoE, UD-Q4_K_M ~21GB, all layers offloaded. Native context 262144.
- name: qwen3.6-35b-a3b-universal-llamacpp-q4
hardware:
gpu_arch: "*"
vram_min_mib: 0
ram_min_mib: 24576
engine: llamacpp
format: gguf
default_config:
quantization: int4
n_gpu_layers: 999
# Full trained context (262144 / 256K). Deploy auto-clamps it down to fit
# hardware memory on smaller machines, so this large default is safe.
ctx_size: 262144
expected_performance:
startup_time_s: 30
cold_start_time_s: 60
tokens_per_second: [55, 70]
latency_first_token_ms: [30, 250]
notes: "Context verified 2026-06-16 on AMD Strix Halo Radeon 8060S iGPU, llama.cpp b9180 HIP, UD-Q4_K_M all 999 layers offloaded: llama-server loads & serves n_ctx=262144 (full trained context)."
# --- GB10 (Blackwell, 128GB unified, Docker/K3S) ---
# 实测稳定路径是 text-only vLLM:
# - language_model_only=true
Expand Down
53 changes: 42 additions & 11 deletions cmd/aima/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -484,19 +486,35 @@ func (a *fleetMCPAdapter) ListToolDefs() json.RawMessage {
// toEngineBinarySource converts a knowledge.EngineSource to engine.BinarySource.
// Centralises the mapping so callers don't repeat the 4-field struct literal.
func toEngineBinarySource(src *knowledge.EngineSource) *engine.BinarySource {
if src == nil {
return nil
}
var probePaths []string
if src != nil && src.Probe != nil {
if src.Probe != nil {
probePaths = append(probePaths, src.Probe.Paths...)
}
return &engine.BinarySource{
Binary: src.Binary,
Platforms: src.Platforms,
Download: src.Download,
Mirror: src.Mirror,
SHA256: src.SHA256,
InstallType: src.InstallType,
ProbePaths: probePaths,
Binary: src.Binary,
Platforms: src.Platforms,
Download: src.Download,
Mirror: src.Mirror,
SHA256: src.SHA256,
InstallType: src.InstallType,
ProbePaths: probePaths,
LocalBundles: engineLocalBundlesFromEnv(),
}
}

func engineLocalBundlesFromEnv() []string {
var bundles []string
for _, name := range []string{"AIMA_ENGINE_BUNDLE", "AIMA_ENGINE_ARCHIVE", "AIMA_ENGINE_OFFLINE_PACKAGE"} {
for _, path := range filepath.SplitList(os.Getenv(name)) {
if path = strings.TrimSpace(path); path != "" {
bundles = append(bundles, path)
}
}
}
return bundles
}

// execRunner implements engine.CommandRunner using real exec.
Expand Down Expand Up @@ -595,6 +613,7 @@ func (a openClawBackendAdapter) ListBackends() map[string]*openclaw.Backend {
result[k] = &openclaw.Backend{
ModelName: b.ModelName,
EngineType: b.EngineType,
ModelType: b.ModelType,
Address: b.Address,
Ready: b.Ready,
Remote: b.Remote,
Expand Down Expand Up @@ -628,7 +647,7 @@ type catalogAdapter struct{ cat *knowledge.Catalog }

func (a catalogAdapter) ModelType(name string) string {
for _, m := range a.cat.ModelAssets {
if strings.EqualFold(m.Metadata.Name, name) {
if catalogModelNameMatches(m, name) {
return m.Metadata.Type
}
}
Expand All @@ -641,7 +660,7 @@ func (a catalogAdapter) ModelContextWindow(name string) int {

func (a catalogAdapter) ModelFamily(name string) string {
for _, m := range a.cat.ModelAssets {
if strings.EqualFold(m.Metadata.Name, name) {
if catalogModelNameMatches(m, name) {
return m.Metadata.Family
}
}
Expand All @@ -650,7 +669,7 @@ func (a catalogAdapter) ModelFamily(name string) string {

func (a catalogAdapter) ModelChatProvider(name string) bool {
for _, m := range a.cat.ModelAssets {
if strings.EqualFold(m.Metadata.Name, name) {
if catalogModelNameMatches(m, name) {
if m.OpenClaw != nil && m.OpenClaw.ChatProvider != nil {
return *m.OpenClaw.ChatProvider
}
Expand All @@ -660,6 +679,18 @@ func (a catalogAdapter) ModelChatProvider(name string) bool {
return true
}

func catalogModelNameMatches(m knowledge.ModelAsset, name string) bool {
if strings.EqualFold(m.Metadata.Name, name) {
return true
}
for _, alias := range m.Metadata.Aliases {
if strings.EqualFold(alias, name) {
return true
}
}
return false
}

func (a catalogAdapter) Adapters(name string) []inferencehttp.Adapter {
for _, m := range a.cat.ModelAssets {
if !strings.EqualFold(m.Metadata.Name, name) || m.HTTP == nil {
Expand Down
12 changes: 7 additions & 5 deletions cmd/aima/compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ func prepareContainerCompatibility(
}

probeSummary := summarizeCompatibilityProbeOutput(probeOutput, probeErr)
if allowAutoPull && len(resolved.EngineRegistries) > 0 && !strings.EqualFold(resolved.EngineDistribution, "local") {
engineRegistries := engineRegistriesWithEnv(resolved.EngineRegistries)
if allowAutoPull && len(engineRegistries) > 0 && !strings.EqualFold(resolved.EngineDistribution, "local") {
if refreshErr := refreshDockerImageForCompatibilityProbe(ctx, runner, resolved); refreshErr == nil {
plan.DockerImageChanged = true
refreshedOutput, refreshedErr := timedContainerCompatibilityProbe(ctx, runner, resolved.CompatibilityProbe, resolved.EngineImage, modelPath, trustRemoteCode, nil)
Expand Down Expand Up @@ -159,7 +160,7 @@ func ensureDockerImageForCompatibilityProbe(ctx context.Context, runner engine.C
if engine.ImageExistsInDocker(ctx, resolved.EngineImage, runner) {
return true, false, nil
}
if !allowAutoPull || len(resolved.EngineRegistries) == 0 || strings.EqualFold(resolved.EngineDistribution, "local") {
if !allowAutoPull || len(engineRegistriesWithEnv(resolved.EngineRegistries)) == 0 || strings.EqualFold(resolved.EngineDistribution, "local") {
return false, false, nil
}
if err := pullDockerImageForCompatibilityProbe(ctx, runner, resolved); err != nil {
Expand All @@ -179,20 +180,21 @@ func pullDockerImageForCompatibilityProbe(ctx context.Context, runner engine.Com
if resolved == nil {
return fmt.Errorf("resolved config is nil")
}
if len(resolved.EngineRegistries) == 0 {
engineRegistries := engineRegistriesWithEnv(resolved.EngineRegistries)
if len(engineRegistries) == 0 {
return fmt.Errorf("no registries configured for %s", resolved.EngineImage)
}
imgName, imgTag := splitImageRef(resolved.EngineImage)
if err := engine.Pull(ctx, engine.PullOptions{
Image: imgName,
Tag: imgTag,
Registries: resolved.EngineRegistries,
Registries: engineRegistries,
Runner: &dockerOnlyRunner{base: runner},
ExpectedDigest: resolved.EngineDigest,
}); err != nil {
return err
}
return ensureDockerImageAlias(ctx, runner, resolved.EngineImage, resolved.EngineRegistries)
return ensureDockerImageAlias(ctx, runner, resolved.EngineImage, engineRegistries)
}

func ensureDockerImageAlias(ctx context.Context, runner engine.CommandRunner, image string, registries []string) error {
Expand Down
105 changes: 105 additions & 0 deletions cmd/aima/ctxfit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"testing"

"github.com/jguan/aima/internal/knowledge"
)

// Qwen2.5-VL-3B text backbone: 36 layers, 2 KV heads, head_dim 128 → 36864 B/token.
const qwen25vl3bKVPerTok = int64(2 * 36 * 2 * 128 * 2)

func TestClampContextForMemory(t *testing.T) {
tests := []struct {
name string
reqCtx int
nCtxTrain int
kvPerTok int64
usableMiB int
nonKVMiB int
want int
clamped bool // want < reqCtx
}{
{
name: "huge memory, capped only by trained context",
reqCtx: 128000, nCtxTrain: 128000, kvPerTok: qwen25vl3bKVPerTok,
usableMiB: 112640, nonKVMiB: 4300, // ~110GB Strix Halo
want: 128000, clamped: false,
},
{
name: "request above trained context is capped down",
reqCtx: 200000, nCtxTrain: 128000, kvPerTok: qwen25vl3bKVPerTok,
usableMiB: 112640, nonKVMiB: 4300,
want: 128000, clamped: true,
},
{
name: "16GB unified clamps below 128k",
reqCtx: 128000, nCtxTrain: 128000, kvPerTok: qwen25vl3bKVPerTok,
usableMiB: 12288, nonKVMiB: 4300, // 16GB - 4GB reserve
// budget = 12288*0.9 - 4300 - 1024 = 5734 MiB → /36864 B = ~163k → but capped at train 128000? 163k>128000 so 128000
want: 128000, clamped: false,
},
{
name: "8GB discrete GPU clamps below 128k",
reqCtx: 128000, nCtxTrain: 128000, kvPerTok: qwen25vl3bKVPerTok,
usableMiB: 8192, nonKVMiB: 4300,
// budget = int(8192*0.9) - 4300 - 1024 = 7372 - 5324 = 2048 MiB
// maxCtx = 2048*1024*1024/36864 = 58254 → round down to 58112
want: 58112, clamped: true,
},
{
name: "tiny memory floors at minCtx",
reqCtx: 128000, nCtxTrain: 128000, kvPerTok: qwen25vl3bKVPerTok,
usableMiB: 6144, nonKVMiB: 4300,
// budget = 6144*0.9 - 4300 - 1024 = ~205 MiB → ~5832 → round 5632
want: 5632, clamped: true,
},
{
name: "unknown memory: only trained-context cap applies",
reqCtx: 65536, nCtxTrain: 128000, kvPerTok: qwen25vl3bKVPerTok,
usableMiB: 0, nonKVMiB: 0,
want: 65536, clamped: false,
},
{
name: "unknown arch (kvPerTok 0): no memory clamp",
reqCtx: 65536, nCtxTrain: 0, kvPerTok: 0,
usableMiB: 8192, nonKVMiB: 4300,
want: 65536, clamped: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, _ := clampContextForMemory(tt.reqCtx, tt.nCtxTrain, tt.kvPerTok, tt.usableMiB, tt.nonKVMiB)
if got != tt.want {
t.Errorf("clampContextForMemory = %d, want %d", got, tt.want)
}
if (got < tt.reqCtx) != tt.clamped {
t.Errorf("clamped = %v, want %v (got=%d req=%d)", got < tt.reqCtx, tt.clamped, got, tt.reqCtx)
}
})
}
}

func TestUsableMemoryMiB(t *testing.T) {
tests := []struct {
name string
hw knowledge.HardwareInfo
want int
}{
{"unified 128GB reserves 16GB cap", knowledge.HardwareInfo{UnifiedMemory: true, RAMTotalMiB: 131072}, 131072 - 16384},
{"unified 16GB reserves 1/4", knowledge.HardwareInfo{UnifiedMemory: true, RAMTotalMiB: 16384}, 16384 - 4096},
{"unified 8GB reserve floored at 2GB", knowledge.HardwareInfo{UnifiedMemory: true, RAMTotalMiB: 8192}, 8192 - 2048},
{"unified APU prefers iGPU pool over under-detected OS RAM", knowledge.HardwareInfo{UnifiedMemory: true, RAMTotalMiB: 32768, GPUVRAMMiB: 110456}, 110456},
{"discrete prefers free VRAM", knowledge.HardwareInfo{GPUVRAMMiB: 8192, GPUMemFreeMiB: 7000}, 7000},
{"discrete falls back to total VRAM", knowledge.HardwareInfo{GPUVRAMMiB: 8192}, 8192},
{"cpu-only uses system RAM", knowledge.HardwareInfo{RAMTotalMiB: 32768}, 32768 - 8192},
{"unknown memory returns 0", knowledge.HardwareInfo{}, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := usableMemoryMiB(tt.hw); got != tt.want {
t.Errorf("usableMemoryMiB = %d, want %d", got, tt.want)
}
})
}
}
Loading
Loading