Phase 0: model migration (qwen3.5-9b-vlm → gemma-4-12b), context alignment (all 262K), routing tiers + MoE spillover, fix dashboard window=1h parsing
This commit is contained in:
+63
-37
@@ -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 (cross-agent spread prevents overheating)
|
"qwen3.6-35B-A3B": 2, # 2 slots (cross-agent spread prevents overheating)
|
||||||
"qwen3.6-27B-code": 2, # 2 slots (128K context frees VRAM)
|
"qwen3.6-27B-code": 2, # 2 slots (128K context frees VRAM)
|
||||||
"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": 262144,
|
"qwen3.6-35B-A3B": 262144,
|
||||||
"qwen3.6-27B-code": 131072,
|
"qwen3.6-27B-code": 262144,
|
||||||
"qwen3.5-9b-vlm": 262144,
|
"gemma-4-12b": 262144,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 loaded from API_KEYS env var (set in docker-compose.yml)
|
# API keys loaded from API_KEYS env var (set in docker-compose.yml)
|
||||||
# Fallback is dev-only — production MUST set API_KEYS env var
|
# Fallback is dev-only — production MUST set API_KEYS env var
|
||||||
@@ -216,22 +216,36 @@ def select_best_gpu(candidates, reason, agent=""):
|
|||||||
return {"model": best, "reason": "load_balanced_" + reason}
|
return {"model": best, "reason": "load_balanced_" + reason}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def route(rd, tier, agent=""):
|
def route(rd, tier, agent=""):
|
||||||
msgs = rd.get("messages",[]); t = estimate_tokens(msgs)
|
msgs = rd.get("messages",[]); t = estimate_tokens(msgs)
|
||||||
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
|
|
||||||
if all(is_gpu_busy(m) for m in avail):
|
if all(is_gpu_busy(m) for m in avail):
|
||||||
return {"model": avail[0], "reason": "all_saturated", "saturated": True}
|
return {"model": avail[0], "reason": "all_saturated", "saturated": True}
|
||||||
|
|
||||||
|
# GUARD: multimodal -> VLM only (sole vision model)
|
||||||
|
has_image = any(
|
||||||
|
isinstance(m.get("content"), list) and
|
||||||
|
any(p.get("type") == "image_url" for p in m["content"] if isinstance(p, dict))
|
||||||
|
for m in msgs
|
||||||
|
)
|
||||||
|
if has_image:
|
||||||
|
if "gemma-4-12b" in avail and not is_gpu_busy("gemma-4-12b"):
|
||||||
|
return {"model": "gemma-4-12b", "reason": "vision"}
|
||||||
|
elif "gemma-4-12b" in avail:
|
||||||
|
return {"model": "gemma-4-12b", "reason": "vision_saturated", "saturated": True}
|
||||||
|
else:
|
||||||
|
return {"model": allowed[0], "reason": "vision_unavailable"}
|
||||||
|
|
||||||
req = rd.get("model","auto")
|
req = rd.get("model","auto")
|
||||||
if req != "auto":
|
if req != "auto":
|
||||||
target = req if req in avail else avail[0]
|
target = req if req in avail else avail[0]
|
||||||
# If explicit model is busy, check if another can take it
|
|
||||||
if is_gpu_busy(target) and req in allowed:
|
if is_gpu_busy(target) and req in allowed:
|
||||||
alts = [m for m in avail if m != target and m in allowed]
|
alts = [m for m in avail if m != target and m in allowed]
|
||||||
if alts:
|
if alts:
|
||||||
@@ -240,50 +254,62 @@ def route(rd, tier, agent=""):
|
|||||||
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", agent) or {"model":"qwen3.5-9b-vlm","reason":"hint_speed"}
|
return select_best_gpu(["gemma-4-12b"], "hint_speed", agent) or {"model":"gemma-4-12b","reason":"hint_speed"}
|
||||||
if hints.get("priority")=="quality" and "qwen3.6-35B-A3B" in avail:
|
if hints.get("priority")=="quality" and "qwen3.6-35B-A3B" in avail:
|
||||||
return select_best_gpu(["qwen3.6-35B-A3B"], "hint_quality", agent) or {"model":"qwen3.6-35B-A3B","reason":"hint_quality"}
|
return select_best_gpu(["qwen3.6-35B-A3B"], "hint_quality", agent) or {"model":"qwen3.6-35B-A3B","reason":"hint_quality"}
|
||||||
|
if hints.get("priority")=="code" and "qwen3.6-27B-code" in avail:
|
||||||
|
return select_best_gpu(["qwen3.6-27B-code"], "hint_code", agent) or {"model":"qwen3.6-27B-code","reason":"hint_code"}
|
||||||
|
|
||||||
first_msg = msgs[0].get("content","") if msgs else ""
|
first_msg = msgs[0].get("content","") if msgs else ""
|
||||||
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 (fastest)
|
# TIER 1: Tiny - single-turn micro queries -> VLM (fastest)
|
||||||
if not sys and turns <= 1 and t <= 500 and words <= 100 and "qwen3.5-9b-vlm" in avail:
|
if not sys and turns <= 1 and t <= 300 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":"tiny"}
|
||||||
# VLM busy — Dense is faster for short queries than MoE
|
|
||||||
fallback = [m for m in ["qwen3.6-27B-code","qwen3.6-35B-A3B"] if m in avail]
|
fallback = [m for m in ["qwen3.6-27B-code","qwen3.6-35B-A3B"] if m in avail]
|
||||||
result = select_best_gpu(fallback, "lightweight_fallback", agent)
|
result = select_best_gpu(fallback, "tiny_fallback", agent)
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
# TIER 2: Simple conversations — VLM primary (up to 15K tok), fastest for moderate chat
|
# TIER 2: Light - moderate chat -> VLM first (fastest), Dense fallback
|
||||||
if t <= 15000 and turns <= 12 and "qwen3.5-9b-vlm" in avail:
|
if t <= 5000 and turns <= 4:
|
||||||
if not is_gpu_busy("qwen3.5-9b-vlm"):
|
candidates = [m for m in ["gemma-4-12b","qwen3.6-27B-code","qwen3.6-35B-A3B"] if m in avail]
|
||||||
return {"model":"qwen3.5-9b-vlm","reason":"simple_conv"}
|
result = select_best_gpu(candidates, "light", agent)
|
||||||
# VLM busy — fall back to Dense, then MoE
|
|
||||||
fallback = [m for m in ["qwen3.6-27B-code","qwen3.6-35B-A3B"] if m in avail]
|
|
||||||
result = select_best_gpu(fallback, "simple_conv_fallback", agent)
|
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
# TIER 3: Medium complexity — Dense primary, VLM fallback (quality + speed balance)
|
# TIER 3: Medium - quality matters -> MoE primary (60%), Dense spillover (40%)
|
||||||
if t <= 25000:
|
if t <= 30000:
|
||||||
candidates = [m for m in ["qwen3.6-27B-code","qwen3.5-9b-vlm","qwen3.6-35B-A3B"] if m in avail]
|
candidates = moe_spillover(avail, ["qwen3.6-35B-A3B","qwen3.6-27B-code","gemma-4-12b"])
|
||||||
result = select_best_gpu(candidates, "medium", agent)
|
result = select_best_gpu(candidates, "medium", agent)
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
# TIER 4: Heavy reasoning — MoE primary (workhorse), Dense fallback
|
# TIER 4: Heavy - quality first -> Dense primary, MoE fallback
|
||||||
if t > 25000:
|
if t > 30000:
|
||||||
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-27B-code","qwen3.6-35B-A3B","gemma-4-12b"] if m in avail]
|
||||||
result = select_best_gpu(candidates, "heavy_reasoning", agent)
|
result = select_best_gpu(candidates, "heavy", agent)
|
||||||
if result: return result
|
if result: return result
|
||||||
|
|
||||||
# TIER 5: Default — Dense primary, MoE fallback
|
# TIER 5: Default - MoE primary (60%), Dense spillover (40%)
|
||||||
candidates = [m for m in ["qwen3.6-27B-code","qwen3.5-9b-vlm","qwen3.6-35B-A3B"] if m in avail]
|
candidates = moe_spillover(avail, ["qwen3.6-35B-A3B","qwen3.6-27B-code","gemma-4-12b"])
|
||||||
result = select_best_gpu(candidates, "default", agent)
|
result = select_best_gpu(candidates, "default", agent)
|
||||||
if result: return result
|
if result: return result
|
||||||
return {"model":avail[0],"reason":"last_resort"}
|
return {"model":avail[0],"reason":"last_resort"}
|
||||||
|
|
||||||
|
|
||||||
|
def moe_spillover(avail, default_order):
|
||||||
|
"""Spill 40% of MoE-first traffic to Dense to prevent Strix Halo overheating.
|
||||||
|
Only applies when MoE is first candidate, available, and not busy."""
|
||||||
|
import random
|
||||||
|
if (default_order[0] == "qwen3.6-35B-A3B"
|
||||||
|
and "qwen3.6-35B-A3B" in avail
|
||||||
|
and not is_gpu_busy("qwen3.6-35B-A3B")
|
||||||
|
and "qwen3.6-27B-code" in avail
|
||||||
|
and not is_gpu_busy("qwen3.6-27B-code")
|
||||||
|
and random.random() < 0.4):
|
||||||
|
# Swap: Dense first, MoE second
|
||||||
|
return ["qwen3.6-27B-code","qwen3.6-35B-A3B"] + [m for m in default_order[2:] if m in avail and m not in ("qwen3.6-27B-code","qwen3.6-35B-A3B")]
|
||||||
|
return [m for m in default_order if m in avail]
|
||||||
def clean_unicode(text):
|
def clean_unicode(text):
|
||||||
if not isinstance(text, str): return text
|
if not isinstance(text, str): return text
|
||||||
text = text.replace(chr(0x2014), "-"); text = text.replace(chr(0x2013), "-")
|
text = text.replace(chr(0x2014), "-"); text = text.replace(chr(0x2013), "-")
|
||||||
@@ -494,7 +520,7 @@ def performance():
|
|||||||
"""Per-request performance analytics with percentiles per model/reason/agent."""
|
"""Per-request performance analytics with percentiles per model/reason/agent."""
|
||||||
if not r: return jsonify({"error": "Redis unavailable"}), 503
|
if not r: return jsonify({"error": "Redis unavailable"}), 503
|
||||||
try:
|
try:
|
||||||
window_hours = int(request.args.get("window", "24"))
|
window_hours = int(request.args.get("window", "24").replace("h",""))
|
||||||
model_filter = request.args.get("model", "all")
|
model_filter = request.args.get("model", "all")
|
||||||
|
|
||||||
# Load recent records
|
# Load recent records
|
||||||
@@ -611,7 +637,7 @@ def scatter():
|
|||||||
"""Return individual data points for scatter plots (prompt_tokens vs latency)."""
|
"""Return individual data points for scatter plots (prompt_tokens vs latency)."""
|
||||||
if not r: return jsonify({"error": "Redis unavailable"}), 503
|
if not r: return jsonify({"error": "Redis unavailable"}), 503
|
||||||
try:
|
try:
|
||||||
window_hours = int(request.args.get("window", "24"))
|
window_hours = int(request.args.get("window", "24").replace("h",""))
|
||||||
model_filter = request.args.get("model", "all")
|
model_filter = request.args.get("model", "all")
|
||||||
cutoff = time.time() - (window_hours * 3600)
|
cutoff = time.time() - (window_hours * 3600)
|
||||||
raw = r.lrange("perf:recent", 0, -1)
|
raw = r.lrange("perf:recent", 0, -1)
|
||||||
|
|||||||
Reference in New Issue
Block a user