VLM migration: qwen3.5-9b-vlm → gemma-4-12b across entire harness
- router/router.py: model ID, context window (131K→262K), VRAM comment - litellm_config.yaml: model name updated - gpu-router.conf, gpu-router-docker.conf: pool names, comments - dashboard/dashboard.py: MODELS array refactor for single-point config - README.md: architecture diagram, model table
This commit is contained in:
@@ -8,7 +8,7 @@ CT 116 Docker stack for routing local GPU models through a unified OpenAI-compat
|
|||||||
nginx :80 → router :9000 → GPU backends
|
nginx :80 → router :9000 → GPU backends
|
||||||
├─ qwen3.6-35B-A3B (MoE) @ 192.168.68.15:8080 [2 slots]
|
├─ qwen3.6-35B-A3B (MoE) @ 192.168.68.15:8080 [2 slots]
|
||||||
├─ qwen3.6-27B-code (Dense) @ 192.168.68.8:8080 [2 slots]
|
├─ qwen3.6-27B-code (Dense) @ 192.168.68.8:8080 [2 slots]
|
||||||
└─ qwen3.5-9b-vlm (VLM) @ 192.168.68.110:8080 [2 slots]
|
└─ gemma-4-12b (VLM) @ 192.168.68.110:8080 [2 slots]
|
||||||
Total: 6 concurrent slots
|
Total: 6 concurrent slots
|
||||||
|
|
||||||
LiteLLM :8081 (fallback) | Dashboard :3000 | Redis :6379 (local)
|
LiteLLM :8081 (fallback) | Dashboard :3000 | Redis :6379 (local)
|
||||||
@@ -63,7 +63,7 @@ When all GPUs are saturated, requests enter a polling queue (500ms intervals) in
|
|||||||
|-----|-------|------|-------|
|
|-----|-------|------|-------|
|
||||||
| Strix Halo | qwen3.6-35B-A3B (MoE) | 65GB | 2 |
|
| Strix Halo | qwen3.6-35B-A3B (MoE) | 65GB | 2 |
|
||||||
| RTX 3090 | qwen3.6-27B-code (Dense) | 24GB | 2 |
|
| RTX 3090 | qwen3.6-27B-code (Dense) | 24GB | 2 |
|
||||||
| RTX 5070 | qwen3.5-9b-vlm (VLM) | 12GB | 2 |
|
| New Backend | gemma-4-12b (VLM) | 7.1GB | 2 |
|
||||||
|
|
||||||
## Maintenance
|
## Maintenance
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -111,9 +111,15 @@ body { background: #0b0f17; color: #bcc3cd; font-family: -apple-system, BlinkMac
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var MC={'gemma-4-12b':'#22c55e','qwen3.6-27B-code':'#f59e0b','qwen3.6-35B-A3B':'#a78bfa'};
|
// ═══ MODEL CONFIG — Update ONLY this array for model migrations ═══
|
||||||
var ML={'gemma-4-12b':'Gemma 4 12B','qwen3.6-27B-code':'Qwen Code','qwen3.6-35B-A3B':'Qwen MoE'};
|
var MODELS=[
|
||||||
var GL={'qwen3.6-35B-A3B':'MoE - Strix Halo','qwen3.6-27B-code':'Dense - RTX 3090','gemma-4-12b':'VLM - New Backend'};
|
{id:'qwen3.6-35B-A3B',label:'Qwen MoE',gpu:'MoE - Strix Halo',color:'#a78bfa',short:'MoE',domId:'gpu-moe'},
|
||||||
|
{id:'qwen3.6-27B-code',label:'Qwen Code',gpu:'Dense - RTX 3090',color:'#f59e0b',short:'Dense',domId:'gpu-dense'},
|
||||||
|
{id:'gemma-4-12b',label:'Gemma 4 12B',gpu:'VLM - New Backend',color:'#22c55e',short:'VLM',domId:'gpu-light'}
|
||||||
|
];
|
||||||
|
// Auto-derived lookups — DO NOT EDIT below
|
||||||
|
var MC={},ML={},GL={},ids={},lb={};
|
||||||
|
MODELS.forEach(function(m){MC[m.id]=m.color;ML[m.id]=m.label;GL[m.id]=m.gpu;ids[m.id]=m.domId;lb[m.id]=m.short;});
|
||||||
function $(id){return document.getElementById(id);}
|
function $(id){return document.getElementById(id);}
|
||||||
|
|
||||||
function render(data){
|
function render(data){
|
||||||
@@ -122,7 +128,6 @@ var t=Object.values(data.route_counts||{}).reduce((a,b)=>a+b,0);
|
|||||||
var ta=0,tm=0;data.gpus.forEach(function(g){ta+=(g.active_requests||0);tm+=(g.max_concurrent||1)});
|
var ta=0,tm=0;data.gpus.forEach(function(g){ta+=(g.active_requests||0);tm+=(g.max_concurrent||1)});
|
||||||
$('kpi-total').textContent=t;$('kpi-active').textContent=ta+'/'+tm;$('kpi-agents').textContent=Object.keys(data.agent_counts||{}).length;
|
$('kpi-total').textContent=t;$('kpi-active').textContent=ta+'/'+tm;$('kpi-agents').textContent=Object.keys(data.agent_counts||{}).length;
|
||||||
$('update-time').textContent=new Date().toLocaleTimeString();
|
$('update-time').textContent=new Date().toLocaleTimeString();
|
||||||
var ids={'qwen3.6-35B-A3B':'gpu-moe','qwen3.6-27B-code':'gpu-dense','gemma-4-12b':'gpu-light'};
|
|
||||||
data.gpus.forEach(function(g){
|
data.gpus.forEach(function(g){
|
||||||
var el=$(ids[g.id]);if(!el)return;
|
var el=$(ids[g.id]);if(!el)return;
|
||||||
var a=g.active_requests||0,mx=g.max_concurrent||1;
|
var a=g.active_requests||0,mx=g.max_concurrent||1;
|
||||||
@@ -161,7 +166,7 @@ el.innerHTML=h;
|
|||||||
|
|
||||||
function renderGPUMetrics(data){
|
function renderGPUMetrics(data){
|
||||||
var el=$('gpu-metrics-card');if(!el)return;
|
var el=$('gpu-metrics-card');if(!el)return;
|
||||||
var lb={'qwen3.6-35B-A3B':'MoE','qwen3.6-27B-code':'Dense','gemma-4-12b':'VLM'};
|
|
||||||
var h='';data.gpus.forEach(function(g){
|
var h='';data.gpus.forEach(function(g){
|
||||||
var nm=lb[g.id]||g.id,tp=g.temp_c||0,ut=g.gpu_util_pct||0,pw=g.power_w||0,pl=g.power_limit_w||0;
|
var nm=lb[g.id]||g.id,tp=g.temp_c||0,ut=g.gpu_util_pct||0,pw=g.power_w||0,pl=g.power_limit_w||0;
|
||||||
var tc=tp>85?'#ef4444':tp>70?'#f59e0b':'#22c55e',uc=ut>90?'#ef4444':ut>70?'#f59e0b':'#22c55e';
|
var tc=tp>85?'#ef4444':tp>70?'#f59e0b':'#22c55e',uc=ut>90?'#ef4444':ut>70?'#f59e0b':'#22c55e';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ upstream llmgpu_pool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upstream ocu_llm_pool {
|
upstream ocu_llm_pool {
|
||||||
## RTX 5070 — qwen3.5-9b-vlm (VLM) — Vision + light tasks
|
## New Backend — gemma-4-12b (VLM) — Vision + light tasks
|
||||||
server 192.168.68.110:8080;
|
server 192.168.68.110:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ map $http_x_syslog_model $gpu_upstream {
|
|||||||
"heavy" llmgpu_pool;
|
"heavy" llmgpu_pool;
|
||||||
"qwen3.5-27B" llmgpu_pool;
|
"qwen3.5-27B" llmgpu_pool;
|
||||||
"light" ocu_llm_pool;
|
"light" ocu_llm_pool;
|
||||||
"qwen3.5-9b-vlm" ocu_llm_pool;
|
"gemma-4-12b" ocu_llm_pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
## Rate limit zone — 10 req/s per IP, burst of 20
|
## Rate limit zone — 10 req/s per IP, burst of 20
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ upstream llmgpu_pool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upstream ocu_llm_pool {
|
upstream ocu_llm_pool {
|
||||||
## RTX 5070 — qwen3.5-9b-vlm (VLM) — Vision + light tasks
|
## New Backend — gemma-4-12b (VLM) — Vision + light tasks
|
||||||
server 192.168.68.110:8080;
|
server 192.168.68.110:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ map $http_x_syslog_model $gpu_upstream {
|
|||||||
"heavy" llmgpu_pool;
|
"heavy" llmgpu_pool;
|
||||||
"qwen3.5-27B" llmgpu_pool;
|
"qwen3.5-27B" llmgpu_pool;
|
||||||
"light" ocu_llm_pool;
|
"light" ocu_llm_pool;
|
||||||
"qwen3.5-9b-vlm" ocu_llm_pool;
|
"gemma-4-12b" ocu_llm_pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|||||||
+2
-2
@@ -11,9 +11,9 @@ model_list:
|
|||||||
api_base: http://192.168.68.8:8080/v1
|
api_base: http://192.168.68.8:8080/v1
|
||||||
api_key: "not-needed"
|
api_key: "not-needed"
|
||||||
|
|
||||||
- model_name: qwen3.5-9b-vlm
|
- model_name: gemma-4-12b
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/qwen3.5-9b-vlm
|
model: openai/gemma-4-12b
|
||||||
api_base: http://192.168.68.110:8080/v1
|
api_base: http://192.168.68.110:8080/v1
|
||||||
api_key: "not-needed"
|
api_key: "not-needed"
|
||||||
|
|
||||||
|
|||||||
+18
-18
@@ -10,31 +10,31 @@ GPU_LIGHT_URL = os.environ.get("GPU_LIGHT_URL", "http://192.168.68.110:8080/v1")
|
|||||||
GPU_SIDECARS = {
|
GPU_SIDECARS = {
|
||||||
"qwen3.6-35B-A3B": "http://192.168.68.15:8090",
|
"qwen3.6-35B-A3B": "http://192.168.68.15:8090",
|
||||||
"qwen3.6-27B-code": "http://192.168.68.8:8090",
|
"qwen3.6-27B-code": "http://192.168.68.8:8090",
|
||||||
"qwen3.5-9b-vlm": "http://192.168.68.110:8090",
|
"gemma-4-12b": "http://192.168.68.110:8090",
|
||||||
}
|
}
|
||||||
GPU_URLS = {
|
GPU_URLS = {
|
||||||
"qwen3.6-35B-A3B": GPU_MOE_URL,
|
"qwen3.6-35B-A3B": GPU_MOE_URL,
|
||||||
"qwen3.6-27B-code": GPU_DENSE_URL,
|
"qwen3.6-27B-code": GPU_DENSE_URL,
|
||||||
"qwen3.5-9b-vlm": GPU_LIGHT_URL,
|
"gemma-4-12b": GPU_LIGHT_URL,
|
||||||
}
|
}
|
||||||
# Max concurrent requests per GPU (based on llama.cpp --parallel)
|
# Max concurrent requests per GPU (based on llama.cpp --parallel)
|
||||||
GPU_MAX_CONCURRENT = {
|
GPU_MAX_CONCURRENT = {
|
||||||
"qwen3.6-35B-A3B": 2, # 2 slots
|
"qwen3.6-35B-A3B": 2, # 2 slots
|
||||||
"qwen3.6-27B-code": 2, # 2 slots
|
"qwen3.6-27B-code": 2, # 2 slots
|
||||||
"qwen3.5-9b-vlm": 2, # 2 slots (12GB VRAM, 4GB headroom)
|
"gemma-4-12b": 2, # 2 slots (7.1GB VRAM)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Context window sizes (tokens) — used for compaction signals
|
# Context window sizes (tokens) — used for compaction signals
|
||||||
GPU_CONTEXT = {
|
GPU_CONTEXT = {
|
||||||
"qwen3.6-35B-A3B": 131072,
|
"qwen3.6-35B-A3B": 131072,
|
||||||
"qwen3.6-27B-code": 98304,
|
"qwen3.6-27B-code": 98304,
|
||||||
"qwen3.5-9b-vlm": 131072,
|
"gemma-4-12b": 262144, # 262K context (gemma-4-12b)
|
||||||
}
|
}
|
||||||
|
|
||||||
TIER_MODELS = {
|
TIER_MODELS = {
|
||||||
"starter": ["qwen3.5-9b-vlm"],
|
"starter": ["gemma-4-12b"],
|
||||||
"professional": ["qwen3.6-35B-A3B", "qwen3.6-27B-code", "qwen3.5-9b-vlm"],
|
"professional": ["qwen3.6-35B-A3B", "qwen3.6-27B-code", "gemma-4-12b"],
|
||||||
"enterprise": ["qwen3.6-35B-A3B", "qwen3.6-27B-code", "qwen3.5-9b-vlm"],
|
"enterprise": ["qwen3.6-35B-A3B", "qwen3.6-27B-code", "gemma-4-12b"],
|
||||||
}
|
}
|
||||||
API_KEYS = {
|
API_KEYS = {
|
||||||
"sk-syslog-local-master-key": {"tier": "enterprise", "agent": "admin"},
|
"sk-syslog-local-master-key": {"tier": "enterprise", "agent": "admin"},
|
||||||
@@ -149,7 +149,7 @@ def route(rd, tier):
|
|||||||
sys = any(m.get("role")=="system" for m in msgs)
|
sys = any(m.get("role")=="system" for m in msgs)
|
||||||
turns = len([m for m in msgs if m.get("role") in ("user","assistant")])
|
turns = len([m for m in msgs if m.get("role") in ("user","assistant")])
|
||||||
hints = rd.get("routing_hints",{})
|
hints = rd.get("routing_hints",{})
|
||||||
allowed = TIER_MODELS.get(tier, ["qwen3.5-9b-vlm"])
|
allowed = TIER_MODELS.get(tier, ["gemma-4-12b"])
|
||||||
avail = [m for m in available_models() if m in allowed]
|
avail = [m for m in available_models() if m in allowed]
|
||||||
if not avail: return {"model": allowed[0], "reason": "all_saturated", "saturated": True}
|
if not avail: return {"model": allowed[0], "reason": "all_saturated", "saturated": True}
|
||||||
# Check if all available GPUs are at max capacity
|
# Check if all available GPUs are at max capacity
|
||||||
@@ -168,8 +168,8 @@ def route(rd, tier):
|
|||||||
return {"model": target, "reason": "explicit"}
|
return {"model": target, "reason": "explicit"}
|
||||||
|
|
||||||
if hints:
|
if hints:
|
||||||
if hints.get("priority")=="speed" and "qwen3.5-9b-vlm" in avail:
|
if hints.get("priority")=="speed" and "gemma-4-12b" in avail:
|
||||||
return select_best_gpu(["qwen3.5-9b-vlm"], "hint_speed") or {"model":"qwen3.5-9b-vlm","reason":"hint_speed"}
|
return select_best_gpu(["gemma-4-12b"], "hint_speed") or {"model":"gemma-4-12b","reason":"hint_speed"}
|
||||||
if hints.get("priority")=="quality" and "qwen3.6-27B-code" in avail:
|
if hints.get("priority")=="quality" and "qwen3.6-27B-code" in avail:
|
||||||
return select_best_gpu(["qwen3.6-27B-code"], "hint_quality") or {"model":"qwen3.6-27B-code","reason":"hint_quality"}
|
return select_best_gpu(["qwen3.6-27B-code"], "hint_quality") or {"model":"qwen3.6-27B-code","reason":"hint_quality"}
|
||||||
|
|
||||||
@@ -177,18 +177,18 @@ def route(rd, tier):
|
|||||||
words = len(first_msg.split()) if isinstance(first_msg, str) else 99
|
words = len(first_msg.split()) if isinstance(first_msg, str) else 99
|
||||||
|
|
||||||
# TIER 1: Lightweight — single-turn short queries → VLM first
|
# TIER 1: Lightweight — single-turn short queries → VLM first
|
||||||
if not sys and turns <= 1 and words <= 100 and "qwen3.5-9b-vlm" in avail:
|
if not sys and turns <= 1 and words <= 100 and "gemma-4-12b" in avail:
|
||||||
if not is_gpu_busy("qwen3.5-9b-vlm"):
|
if not is_gpu_busy("gemma-4-12b"):
|
||||||
return {"model":"qwen3.5-9b-vlm","reason":"lightweight"}
|
return {"model":"gemma-4-12b","reason":"lightweight"}
|
||||||
# VLM busy — fall back to Dense, then MoE
|
# VLM busy — fall back to Dense, then MoE
|
||||||
fallback = [m for m in ["qwen3.6-35B-A3B","qwen3.6-27B-code"] if m in avail]
|
fallback = [m for m in ["qwen3.6-35B-A3B","qwen3.6-27B-code"] if m in avail]
|
||||||
result = select_best_gpu(fallback, "lightweight_fallback")
|
result = select_best_gpu(fallback, "lightweight_fallback")
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
# TIER 2: Simple conversations — short context, any prompt → VLM preferred
|
# TIER 2: Simple conversations — short context, any prompt → VLM preferred
|
||||||
if t <= 1000 and turns <= 4 and "qwen3.5-9b-vlm" in avail:
|
if t <= 1000 and turns <= 4 and "gemma-4-12b" in avail:
|
||||||
if not is_gpu_busy("qwen3.5-9b-vlm"):
|
if not is_gpu_busy("gemma-4-12b"):
|
||||||
return {"model":"qwen3.5-9b-vlm","reason":"simple_conv"}
|
return {"model":"gemma-4-12b","reason":"simple_conv"}
|
||||||
# VLM busy — try Dense
|
# VLM busy — try Dense
|
||||||
if "qwen3.6-27B-code" in avail and not is_gpu_busy("qwen3.6-27B-code"):
|
if "qwen3.6-27B-code" in avail and not is_gpu_busy("qwen3.6-27B-code"):
|
||||||
return {"model":"qwen3.6-27B-code","reason":"simple_conv_fallback"}
|
return {"model":"qwen3.6-27B-code","reason":"simple_conv_fallback"}
|
||||||
@@ -196,13 +196,13 @@ def route(rd, tier):
|
|||||||
# TIER 3: Heavy reasoning — extremely large context or very long conversations
|
# TIER 3: Heavy reasoning — extremely large context or very long conversations
|
||||||
if t > 50000 or turns > 25:
|
if t > 50000 or turns > 25:
|
||||||
# MoE first (131K context handles heavy sessions), then Dense (98K reasoning), then Light (131K fallback)
|
# MoE first (131K context handles heavy sessions), then Dense (98K reasoning), then Light (131K fallback)
|
||||||
candidates = [m for m in ["qwen3.6-35B-A3B","qwen3.6-27B-code","qwen3.5-9b-vlm"] if m in avail]
|
candidates = [m for m in ["qwen3.6-35B-A3B","qwen3.6-27B-code","gemma-4-12b"] if m in avail]
|
||||||
result = select_best_gpu(candidates, "heavy_reasoning")
|
result = select_best_gpu(candidates, "heavy_reasoning")
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
# TIER 4: Default — MoE first, VLM helps, Dense last (slow)
|
# TIER 4: Default — MoE first, VLM helps, Dense last (slow)
|
||||||
if t <= 50000:
|
if t <= 50000:
|
||||||
candidates = [m for m in ["qwen3.6-35B-A3B","qwen3.5-9b-vlm","qwen3.6-27B-code"] if m in avail]
|
candidates = [m for m in ["qwen3.6-35B-A3B","gemma-4-12b","qwen3.6-27B-code"] if m in avail]
|
||||||
result = select_best_gpu(candidates, "default")
|
result = select_best_gpu(candidates, "default")
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user