Harden framework: tests, VRAM Python, dashboard spec, security, CI
- Rewrite vram_detect in Python with fixed config parsing and 10KB read limit
- Add pytest suite (72 tests) covering dashboard core, app security, and VRAM
- Standardize all prompts to .automaton/tasks/{task-name}/ path
- Reconcile dashboard spec with web implementation; remove themes.py
- Remove half-implemented refresh.py file watcher
- Harden dashboard static-file serving and task-name validation
- Add uncommitted-change guard to update.sh and real Gitea URLs
- Add AGENTS.md, Gitea CI workflow, and template documentation
This commit is contained in:
+15
-15
@@ -10,7 +10,7 @@ if [ -d "$FRAMEWORK_DIR" ]; then
|
||||
fi
|
||||
|
||||
echo "Cloning automaton to $FRAMEWORK_DIR..."
|
||||
git clone https://gitea.yourdomain.com/you/automaton.git "$FRAMEWORK_DIR"
|
||||
git clone http://10.37.0.86:3003/hermes/automaton "$FRAMEWORK_DIR"
|
||||
|
||||
echo ""
|
||||
echo "=== VRAM / Context Detection ==="
|
||||
@@ -18,23 +18,23 @@ echo "Detecting your system's VRAM to recommend task decomposition settings..."
|
||||
echo ""
|
||||
|
||||
# Run VRAM detection script if it exists
|
||||
if [ -f "$FRAMEWORK_DIR/scripts/vram_detect.sh" ]; then
|
||||
if [ -f "$FRAMEWORK_DIR/scripts/vram_detect.py" ]; then
|
||||
# Run in project-dir context so it can read framework overhead
|
||||
detection_output=$(cd "$FRAMEWORK_DIR" && bash "$FRAMEWORK_DIR/scripts/vram_detect.sh" 2>&1)
|
||||
|
||||
# Extract JSON output (last section after "=== JSON Output ===")
|
||||
json_output=$(echo "$detection_output" | sed -n '/=== JSON Output ===/,/EOF/p' | grep -v '=== JSON Output ===' | grep -v '^EOF$')
|
||||
|
||||
detection_output=$(cd "$FRAMEWORK_DIR" && python3 "$FRAMEWORK_DIR/scripts/vram_detect.py" 2>&1)
|
||||
|
||||
# Extract JSON output (the block after "=== JSON Output ===")
|
||||
json_output=$(echo "$detection_output" | sed -n '/=== JSON Output ===/,$p' | tail -n +2)
|
||||
|
||||
if [ -n "$json_output" ]; then
|
||||
echo "$detection_output"
|
||||
|
||||
# Extract key values from JSON for display
|
||||
recommended_k=$(echo "$json_output" | grep '"recommended_k"' | grep -oP '\d+')
|
||||
max_peak_kb=$(echo "$json_output" | grep '"max_peak_context_kb"' | grep -oP '\d+')
|
||||
headroom=$(echo "$json_output" | grep '"headroom"' | grep -oP '\d+\.\d+')
|
||||
gpu_vram=$(echo "$json_output" | grep '"gpu_vram_gb"' | grep -oP '\d+')
|
||||
ram_gb=$(echo "$json_output" | grep '"ram_gb"' | grep -oP '\d+')
|
||||
model_context=$(echo "$json_output" | grep '"model_context_kb"' | grep -oP '\d+')
|
||||
|
||||
# Extract key values from JSON using Python
|
||||
recommended_k=$(echo "$json_output" | python3 -c 'import json,sys; print(json.load(sys.stdin)["recommended_k"])')
|
||||
max_peak_kb=$(echo "$json_output" | python3 -c 'import json,sys; print(json.load(sys.stdin)["max_peak_context_kb"])')
|
||||
headroom=$(echo "$json_output" | python3 -c 'import json,sys; print(json.load(sys.stdin)["headroom"])')
|
||||
gpu_vram=$(echo "$json_output" | python3 -c 'import json,sys; print(json.load(sys.stdin)["gpu_vram_gb"])')
|
||||
ram_gb=$(echo "$json_output" | python3 -c 'import json,sys; print(json.load(sys.stdin)["ram_gb"])')
|
||||
model_context=$(echo "$json_output" | python3 -c 'import json,sys; print(json.load(sys.stdin)["model_context_kb"])')
|
||||
|
||||
echo ""
|
||||
echo "=== Recommended VRAM Configuration ==="
|
||||
|
||||
@@ -19,6 +19,13 @@ if [ ! -d ".git" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for uncommitted changes
|
||||
if ! git diff --quiet HEAD || ! git diff --cached --quiet HEAD; then
|
||||
echo "ERROR: $FRAMEWORK_DIR has uncommitted changes."
|
||||
echo "Commit or stash them before updating to avoid losing work."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch and pull latest changes
|
||||
git pull origin main
|
||||
|
||||
|
||||
Executable
+536
@@ -0,0 +1,536 @@
|
||||
#!/usr/bin/env python3
|
||||
"""VRAM/Context Detection Script for automaton.
|
||||
|
||||
Detects GPU VRAM, system RAM, and model context window to recommend a safe
|
||||
context window for task decomposition.
|
||||
|
||||
Usage:
|
||||
python vram_detect.py [model_name] [--model model_name] [--project project_dir]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Known model context windows in tokens.
|
||||
MODEL_CONTEXT_WINDOWS: dict[str, int] = {
|
||||
"gpt-4o": 128_000,
|
||||
"gpt-4o-2024-05-13": 128_000,
|
||||
"gpt-4o-2024-08-06": 128_000,
|
||||
"gpt-4o-mini": 128_000,
|
||||
"gpt-4o-mini-2024-07-18": 128_000,
|
||||
"gpt-4-turbo": 128_000,
|
||||
"gpt-4-turbo-2024-04-09": 128_000,
|
||||
"gpt-4": 128_000,
|
||||
"gpt-4-0125-preview": 128_000,
|
||||
"gpt-4-1106-preview": 128_000,
|
||||
"claude-3-5-sonnet": 200_000,
|
||||
"claude-3-5-sonnet-20241022": 200_000,
|
||||
"claude-3-5-haiku": 200_000,
|
||||
"claude-3-5-haiku-20241022": 200_000,
|
||||
"claude-3-opus": 200_000,
|
||||
"claude-3-opus-20240229": 200_000,
|
||||
"claude-3-sonnet": 200_000,
|
||||
"claude-3-sonnet-20240229": 200_000,
|
||||
"claude-3-haiku": 200_000,
|
||||
"claude-3-haiku-20240307": 200_000,
|
||||
"claude-2": 200_000,
|
||||
"claude-2.1": 200_000,
|
||||
}
|
||||
|
||||
DEFAULT_FALLBACK_CONTEXT_TOKENS = 128_000
|
||||
DEFAULT_HEADROOM_PCT = 25
|
||||
MAX_CONFIG_READ_BYTES = 10 * 1024 # 10KB limit per prompt requirement
|
||||
|
||||
|
||||
def run_command(cmd: list[str], timeout: float = 5.0) -> Optional[str]:
|
||||
"""Run a command and return stdout, or None on failure."""
|
||||
if not shutil.which(cmd[0]):
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return result.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def detect_gpu_vram() -> tuple[int, int, int]:
|
||||
"""Detect GPU VRAM and return (total_vram_kb, vram_per_gpu_kb, num_gpus)."""
|
||||
total_vram_kb = 0
|
||||
num_gpus = 0
|
||||
|
||||
# Try nvidia-smi first.
|
||||
nvidia_output = run_command(
|
||||
["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
)
|
||||
if nvidia_output:
|
||||
lines = [line.strip() for line in nvidia_output.splitlines() if line.strip()]
|
||||
if lines:
|
||||
try:
|
||||
vram_mb = int(lines[0])
|
||||
if vram_mb > 0:
|
||||
total_vram_kb = vram_mb * 1024
|
||||
num_gpus = len(lines)
|
||||
print("GPU: NVIDIA (nvidia-smi available)")
|
||||
print(f"VRAM per GPU: {vram_mb // 1024}GB ({vram_mb} MB)")
|
||||
print(f"Num GPUs: {num_gpus}")
|
||||
except ValueError:
|
||||
print("GPU: NVIDIA (nvidia-smi available but driver not responding)")
|
||||
|
||||
# Fallback: lspci for AMD/others.
|
||||
if total_vram_kb == 0:
|
||||
lspci_output = run_command(["lspci"])
|
||||
if lspci_output and re.search(r"VGA|3D|Display", lspci_output, re.IGNORECASE):
|
||||
print("GPU detected via lspci")
|
||||
vram_mb = _detect_amd_vram_from_lspci()
|
||||
if vram_mb > 0:
|
||||
total_vram_kb = vram_mb * 1024
|
||||
num_gpus = 1
|
||||
print(f"VRAM: {vram_mb} MB ({vram_mb // 1024}GB)")
|
||||
|
||||
vram_per_gpu_kb = total_vram_kb // num_gpus if num_gpus > 0 else 0
|
||||
return total_vram_kb, vram_per_gpu_kb, num_gpus
|
||||
|
||||
|
||||
def _detect_amd_vram_from_lspci() -> int:
|
||||
"""Attempt to sum memory region sizes from lspci -vnn for VGA/3D/Display devices."""
|
||||
output = run_command(["lspci", "-vnn"])
|
||||
if not output:
|
||||
return 0
|
||||
|
||||
total_mb = 0
|
||||
# Split into device blocks. Each block starts with a bus address like "71:00.0".
|
||||
blocks = re.split(r"\n\n", output)
|
||||
for block in blocks:
|
||||
if not re.search(r"VGA|3D|Display", block, re.IGNORECASE):
|
||||
continue
|
||||
for line in block.splitlines():
|
||||
match = re.search(r"Memory at [0-9a-fA-Fx]+ \(.*\) \[size=(\d+)([MGK])\]", line)
|
||||
if match:
|
||||
value = int(match.group(1))
|
||||
unit = match.group(2).upper()
|
||||
if unit == "G":
|
||||
total_mb += value * 1024
|
||||
elif unit == "M":
|
||||
total_mb += value
|
||||
elif unit == "K":
|
||||
total_mb += value // 1024
|
||||
return total_mb
|
||||
|
||||
|
||||
def detect_ram() -> tuple[int, int]:
|
||||
"""Detect total and available RAM in KB."""
|
||||
meminfo = Path("/proc/meminfo")
|
||||
if meminfo.exists():
|
||||
try:
|
||||
text = meminfo.read_text(encoding="utf-8")
|
||||
total_kb = _parse_meminfo_value(text, "MemTotal")
|
||||
available_kb = _parse_meminfo_value(text, "MemAvailable") or total_kb
|
||||
if total_kb > 0:
|
||||
print(f"RAM: {total_kb // 1024 // 1024}GB total, {available_kb // 1024 // 1024}GB available")
|
||||
return total_kb, available_kb
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
sysctl_output = run_command(["sysctl", "-n", "hw.memsize"])
|
||||
if sysctl_output:
|
||||
try:
|
||||
total_kb = int(sysctl_output) // 1024
|
||||
if total_kb > 0:
|
||||
print(f"RAM: {total_kb // 1024 // 1024}GB total")
|
||||
return total_kb, total_kb
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
print("RAM: Could not detect")
|
||||
return 0, 0
|
||||
|
||||
|
||||
def _parse_meminfo_value(text: str, key: str) -> int:
|
||||
"""Parse a value in KB from /proc/meminfo."""
|
||||
for line in text.splitlines():
|
||||
if line.startswith(key + ":"):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
return int(parts[1])
|
||||
return 0
|
||||
|
||||
|
||||
def detect_model_context(
|
||||
model_name: Optional[str] = None,
|
||||
project_dir: Optional[Path] = None,
|
||||
) -> int:
|
||||
"""Detect model context window in tokens."""
|
||||
if model_name:
|
||||
return _lookup_model_context(model_name)
|
||||
|
||||
# Try config.md (global framework model settings).
|
||||
config_md = Path.home() / ".automaton" / "config.md"
|
||||
model_from_config, override_context = _parse_config_model(config_md)
|
||||
if model_from_config:
|
||||
print(f"Found model in config.md: {model_from_config}")
|
||||
if override_context and override_context != "auto":
|
||||
print(f"Using override context window from config.md: {override_context}")
|
||||
return override_context
|
||||
return _lookup_model_context(model_from_config)
|
||||
|
||||
# Try .agent.md (project-level model override).
|
||||
if project_dir:
|
||||
agent_md = project_dir / ".automaton" / ".agent.md"
|
||||
model_from_agent, _ = _parse_config_model(agent_md)
|
||||
if model_from_agent:
|
||||
print(f"Found model in .agent.md: {model_from_agent}")
|
||||
if override_context and override_context != "auto":
|
||||
print(f"Using override context window from config.md: {override_context}")
|
||||
return override_context
|
||||
return _lookup_model_context(model_from_agent)
|
||||
|
||||
# Try common API config files.
|
||||
config_files = [
|
||||
".env",
|
||||
".env.local",
|
||||
"config.yaml",
|
||||
"config.yml",
|
||||
"config.json",
|
||||
"settings.yaml",
|
||||
".automaton/config.yaml",
|
||||
".automaton/config.json",
|
||||
]
|
||||
if project_dir:
|
||||
for config_file in config_files:
|
||||
for candidate in [project_dir / config_file, project_dir / ".automaton" / config_file]:
|
||||
if candidate.exists():
|
||||
model = _extract_model_from_file(candidate)
|
||||
if model:
|
||||
print(f"Found model in {candidate}: {model}")
|
||||
if override_context and override_context != "auto":
|
||||
print(f"Using override context window from config.md: {override_context}")
|
||||
return override_context
|
||||
return _lookup_model_context(model)
|
||||
|
||||
print("Model: Unknown (could not detect from .agent.md or config files)")
|
||||
return 0
|
||||
|
||||
|
||||
def _lookup_model_context(model_name: str) -> int:
|
||||
"""Look up context window for a known model name."""
|
||||
# Strip common version/date suffixes for lookup.
|
||||
for key in MODEL_CONTEXT_WINDOWS:
|
||||
if model_name.lower().startswith(key.lower()):
|
||||
print(f"Model: {model_name}")
|
||||
print(f"Context window: {MODEL_CONTEXT_WINDOWS[key] // 1000}k tokens")
|
||||
return MODEL_CONTEXT_WINDOWS[key]
|
||||
print(f"Model: {model_name} (unknown context window)")
|
||||
return 0
|
||||
|
||||
|
||||
def _parse_config_model(config_path: Path) -> tuple[Optional[str], Optional[int]]:
|
||||
"""Parse model name and override context window from a config file."""
|
||||
if not config_path.exists():
|
||||
return None, None
|
||||
try:
|
||||
text = config_path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return None, None
|
||||
|
||||
model_name = None
|
||||
override_tokens: Optional[int] = None
|
||||
in_code_block = False
|
||||
|
||||
for raw_line in text.splitlines():
|
||||
line = raw_line.strip()
|
||||
if line.startswith("```"):
|
||||
in_code_block = not in_code_block
|
||||
continue
|
||||
if in_code_block:
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if re.search(r"^[-*]?\s*\*\*Model\*\*:|^\s*model\s*[:=]", line, re.IGNORECASE):
|
||||
if "override" in line.lower():
|
||||
continue
|
||||
value = _extract_value(line)
|
||||
if value and value.lower() != "auto":
|
||||
model_name = value
|
||||
if re.search(r"^[-*]?\s*\*\*Override context( window)?\*\*:|^\s*override.*context\s*[:=]", line, re.IGNORECASE):
|
||||
value = _extract_value(line)
|
||||
if value and value.lower() != "auto":
|
||||
override_tokens = _parse_token_value(value)
|
||||
|
||||
return model_name, override_tokens
|
||||
|
||||
|
||||
def _extract_model_from_file(path: Path) -> Optional[str]:
|
||||
"""Extract model name from an API config file, reading at most 10KB."""
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
text = raw[:MAX_CONFIG_READ_BYTES].decode("utf-8", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if re.search(r"model\s*[:=]", line, re.IGNORECASE):
|
||||
# Skip non-model keys like max_tokens, temperature, stream.
|
||||
key = line.split("=", 1)[0].split(":", 1)[0].strip().lower()
|
||||
if any(bad in key for bad in ["context", "max_tokens", "temperature", "stream"]):
|
||||
continue
|
||||
value = _extract_value(line)
|
||||
if value and value.lower() != "auto":
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _extract_value(line: str) -> Optional[str]:
|
||||
"""Extract the value after ':' or '=' from a key-value line."""
|
||||
for sep in [":", "="]:
|
||||
if sep in line:
|
||||
value = line.split(sep, 1)[1].strip()
|
||||
value = value.split("#", 1)[0].strip()
|
||||
value = value.strip('"').strip("'")
|
||||
return value if value else None
|
||||
return None
|
||||
|
||||
|
||||
def _parse_token_value(value: str) -> Optional[int]:
|
||||
"""Parse a token value like '128k', '5.6k', or '128000' into an integer."""
|
||||
value = value.strip().lower()
|
||||
match = re.search(r"(\d+(?:\.\d+)?)\s*k?", value)
|
||||
if not match:
|
||||
return None
|
||||
number = float(match.group(1))
|
||||
if "k" in value:
|
||||
return int(number * 1000)
|
||||
return int(number)
|
||||
|
||||
|
||||
def calculate_overhead(project_dir: Optional[Path] = None) -> int:
|
||||
"""Estimate framework overhead in tokens."""
|
||||
if project_dir:
|
||||
base_files = [
|
||||
project_dir / ".automaton" / ".agent.md",
|
||||
project_dir / ".automaton" / ".rules.md",
|
||||
project_dir / ".automaton" / "prompts" / "workflow.md",
|
||||
project_dir / ".automaton" / "prompts" / "orchestrate.md",
|
||||
]
|
||||
if not base_files[0].exists():
|
||||
base_files = [
|
||||
Path.home() / ".automaton" / ".agent.md",
|
||||
Path.home() / ".automaton" / ".rules.md",
|
||||
Path.home() / ".automaton" / "prompts" / "workflow.md",
|
||||
Path.home() / ".automaton" / "prompts" / "orchestrate.md",
|
||||
]
|
||||
else:
|
||||
base_files = [
|
||||
Path.home() / ".automaton" / ".agent.md",
|
||||
Path.home() / ".automaton" / ".rules.md",
|
||||
Path.home() / ".automaton" / "prompts" / "workflow.md",
|
||||
Path.home() / ".automaton" / "prompts" / "orchestrate.md",
|
||||
]
|
||||
|
||||
overhead_tokens = 0
|
||||
for file_path in base_files:
|
||||
if file_path.exists():
|
||||
try:
|
||||
chars = len(file_path.read_text(encoding="utf-8", errors="replace"))
|
||||
tokens = chars // 4
|
||||
overhead_tokens += tokens
|
||||
print(f" {file_path.name}: ~{tokens} tokens")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print(f"Framework overhead: ~{overhead_tokens} tokens")
|
||||
return overhead_tokens
|
||||
|
||||
|
||||
def _iter_config_lines(text: str, section_header: str) -> list[str]:
|
||||
"""Return non-code-block lines within a markdown section."""
|
||||
lines: list[str] = []
|
||||
in_section = False
|
||||
in_code_block = False
|
||||
for raw_line in text.splitlines():
|
||||
line = raw_line.strip()
|
||||
if line.startswith("```"):
|
||||
in_code_block = not in_code_block
|
||||
continue
|
||||
if in_code_block:
|
||||
continue
|
||||
if line.startswith(section_header):
|
||||
in_section = True
|
||||
continue
|
||||
if in_section and line.startswith("##"):
|
||||
in_section = False
|
||||
if not in_section:
|
||||
continue
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def parse_vram_config(config_path: Path) -> dict[str, object]:
|
||||
"""Parse VRAM configuration from config.md."""
|
||||
defaults: dict[str, object] = {
|
||||
"auto_detect": True,
|
||||
"target_context_kb": 0,
|
||||
"headroom_pct": DEFAULT_HEADROOM_PCT,
|
||||
"max_peak_kb": 0,
|
||||
}
|
||||
if not config_path.exists():
|
||||
return defaults
|
||||
|
||||
try:
|
||||
text = config_path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return defaults
|
||||
|
||||
for line in _iter_config_lines(text, "## VRAM Configuration"):
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
lower = line.lower()
|
||||
if "auto-detect" in lower:
|
||||
value = _extract_value(line)
|
||||
if value:
|
||||
defaults["auto_detect"] = value.lower() in ("yes", "true", "1")
|
||||
elif "target" in lower and "context" in lower:
|
||||
value = _extract_value(line)
|
||||
if value:
|
||||
parsed = _parse_token_value(value)
|
||||
if parsed:
|
||||
defaults["target_context_kb"] = parsed
|
||||
elif "headroom" in lower:
|
||||
value = _extract_value(line)
|
||||
if value:
|
||||
match = re.search(r"(\d+)", value)
|
||||
if match:
|
||||
defaults["headroom_pct"] = int(match.group(1))
|
||||
elif "max peak" in lower:
|
||||
value = _extract_value(line)
|
||||
if value:
|
||||
parsed = _parse_token_value(value)
|
||||
if parsed:
|
||||
defaults["max_peak_kb"] = parsed
|
||||
|
||||
return defaults
|
||||
|
||||
|
||||
def recommend_context(
|
||||
gpu_vram_gb: int,
|
||||
ram_gb: int,
|
||||
model_context_kb: int,
|
||||
overhead_tokens: int,
|
||||
config: dict[str, object],
|
||||
) -> tuple[int, int, int]:
|
||||
"""Return (headroom_pct, recommended_kb, max_peak_kb)."""
|
||||
headroom_pct = int(config.get("headroom_pct", DEFAULT_HEADROOM_PCT))
|
||||
|
||||
# Manual override mode.
|
||||
if not config.get("auto_detect", True):
|
||||
target_kb = int(config.get("target_context_kb", 0))
|
||||
max_peak_kb = int(config.get("max_peak_kb", 0))
|
||||
if target_kb > 0:
|
||||
if max_peak_kb == 0 and headroom_pct > 0:
|
||||
max_peak_kb = target_kb * (100 - headroom_pct) // 100
|
||||
return headroom_pct, target_kb, max_peak_kb
|
||||
|
||||
recommended_kb = 0
|
||||
|
||||
if gpu_vram_gb >= 4:
|
||||
# Conservative: 1GB VRAM ≈ 2k context tokens.
|
||||
vram_context_kb = gpu_vram_gb * 2000
|
||||
recommended_kb = vram_context_kb * (100 - headroom_pct) // 100
|
||||
elif model_context_kb > 0:
|
||||
recommended_kb = model_context_kb * (100 - headroom_pct) // 100
|
||||
else:
|
||||
# RAM fallback: 0.75k tokens per GB.
|
||||
ram_context_kb = ram_gb * 750
|
||||
recommended_kb = ram_context_kb * (100 - headroom_pct) // 100
|
||||
|
||||
# Subtract framework overhead.
|
||||
net_kb = max(0, recommended_kb - overhead_tokens)
|
||||
|
||||
# Calculate max peak context based on headroom.
|
||||
max_peak_kb = net_kb * (100 - headroom_pct) // 100
|
||||
|
||||
return headroom_pct, net_kb, max_peak_kb
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Detect VRAM/context for automaton")
|
||||
parser.add_argument("model", nargs="?", help="Model name")
|
||||
parser.add_argument("--model", "-m", dest="model_flag", help="Model name")
|
||||
parser.add_argument("--project", "-p", type=Path, help="Project directory")
|
||||
args = parser.parse_args()
|
||||
|
||||
model_name = args.model_flag or args.model
|
||||
project_dir = args.project.resolve() if args.project else None
|
||||
|
||||
print("=== VRAM / Context Detection ===\n")
|
||||
|
||||
print("--- GPU VRAM ---")
|
||||
total_vram_kb, vram_per_gpu_kb, num_gpus = detect_gpu_vram()
|
||||
gpu_vram_gb = total_vram_kb // 1024 // 1024
|
||||
print(f"Total VRAM: {gpu_vram_gb}GB")
|
||||
print(f"VRAM per GPU: {vram_per_gpu_kb // 1024 // 1024}GB")
|
||||
print(f"Num GPUs: {num_gpus}\n")
|
||||
|
||||
print("--- RAM ---")
|
||||
ram_kb, _ = detect_ram()
|
||||
ram_gb = ram_kb // 1024 // 1024
|
||||
print(f"RAM: {ram_gb}GB\n")
|
||||
|
||||
print("--- Model Context Window ---")
|
||||
model_context_kb = detect_model_context(model_name, project_dir)
|
||||
print()
|
||||
|
||||
print("--- Framework Overhead ---")
|
||||
overhead_tokens = calculate_overhead(project_dir)
|
||||
print()
|
||||
|
||||
print("--- Recommendation ---")
|
||||
config_path = Path.home() / ".automaton" / "config.md"
|
||||
config = parse_vram_config(config_path)
|
||||
headroom_pct, recommended_kb, max_peak_kb = recommend_context(
|
||||
gpu_vram_gb, ram_gb, model_context_kb, overhead_tokens, config
|
||||
)
|
||||
|
||||
recommended_k = recommended_kb // 1000 if recommended_kb > 0 else 8
|
||||
max_peak_k = max_peak_kb // 1000 if max_peak_kb > 0 else 6
|
||||
|
||||
print(f"Target context: {recommended_k}k tokens")
|
||||
print(f"Headroom: {headroom_pct}%")
|
||||
print(f"Max peak context per sub-task: {max_peak_k}k tokens")
|
||||
|
||||
print("\n=== JSON Output ===")
|
||||
output = {
|
||||
"gpu_vram_gb": gpu_vram_gb,
|
||||
"ram_gb": ram_gb,
|
||||
"model_context_kb": model_context_kb,
|
||||
"framework_overhead_tokens": overhead_tokens,
|
||||
"recommended_kb": recommended_kb,
|
||||
"recommended_k": recommended_k,
|
||||
"headroom": headroom_pct / 100.0,
|
||||
"max_peak_context_kb": max_peak_kb,
|
||||
}
|
||||
print(json.dumps(output, indent=4))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,549 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# VRAM/Context Detection Script
|
||||
# Detects GPU VRAM, system RAM, and model context window to recommend
|
||||
# a safe VRAM context window for task decomposition.
|
||||
#
|
||||
# Usage: ./vram_detect.sh [model_name]
|
||||
# - If model_name is provided, looks up its context window
|
||||
# - Otherwise, tries to detect from API config or config.md
|
||||
|
||||
set -uo pipefail # Don't exit on error - we want to continue even if detection fails
|
||||
|
||||
# ─── GPU VRAM Detection ───
|
||||
detect_gpu_vram() {
|
||||
local total_vram_kb=0
|
||||
local vram_per_gpu_kb=0
|
||||
local num_gpus=0
|
||||
|
||||
# Try nvidia-smi first (NVIDIA GPUs)
|
||||
if command -v nvidia-smi &>/dev/null; then
|
||||
local vram_kb
|
||||
# Use timeout to avoid hanging on nvidia-smi (e.g., driver not loaded)
|
||||
vram_kb=$(timeout 5 nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ' || true)
|
||||
# Validate that vram_kb is a positive number
|
||||
if [[ -n "$vram_kb" && "$vram_kb" =~ ^[0-9]+$ && "$vram_kb" -gt 0 ]]; then
|
||||
total_vram_kb=$((vram_kb * 1024)) # MB → KB
|
||||
vram_per_gpu_kb=$((total_vram_kb / (num_gpus+1)))
|
||||
num_gpus=1
|
||||
echo "GPU: NVIDIA (nvidia-smi available)"
|
||||
echo "VRAM per GPU: $((vram_kb / 1024))GB ($vram_kb MB)"
|
||||
else
|
||||
echo "GPU: NVIDIA (nvidia-smi available but driver not responding)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: lspci
|
||||
if [[ $total_vram_kb -eq 0 && $num_gpus -eq 0 ]]; then
|
||||
local gpu_info
|
||||
gpu_info=$(lspci 2>/dev/null | grep -i -E 'VGA|3D|Display' | head -5)
|
||||
if [[ -n "$gpu_info" ]]; then
|
||||
echo "GPU detected: $gpu_info"
|
||||
# Try to get VRAM from lspci -vnn memory regions
|
||||
# GPUs show VRAM as Memory regions in lspci
|
||||
# Parse patterns like: Memory at f800000000 (64-bit, prefetchable) [size=256M]
|
||||
local total_vram_mb=0
|
||||
while IFS= read -r line; do
|
||||
# Extract the size value from [size=256M] pattern
|
||||
local size_num
|
||||
size_num=$(echo "$line" | grep -oE '[0-9]+(M|G|K)' | head -1 || true)
|
||||
if [[ -n "$size_num" ]]; then
|
||||
local size_val
|
||||
size_val=$(echo "$size_num" | grep -oE '[0-9]+')
|
||||
local size_unit
|
||||
size_unit=$(echo "$size_num" | grep -oE '(M|G|K)')
|
||||
if [[ -n "$size_val" && -n "$size_unit" ]]; then
|
||||
case "$size_unit" in
|
||||
M) total_vram_mb=$((total_vram_mb + size_val)) ;;
|
||||
G) total_vram_mb=$((total_vram_mb + size_val * 1024)) ;;
|
||||
K) total_vram_mb=$((total_vram_mb + size_val / 1024)) ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
done < <(lspci -vnn 2>/dev/null | grep -i -A 15 -E 'VGA|3D|Display' | grep -i 'Memory at')
|
||||
if [[ $total_vram_mb -gt 0 ]]; then
|
||||
local total_vram_gb=$((total_vram_mb / 1024))
|
||||
local total_vram_mb_remain=$((total_vram_mb % 1024))
|
||||
echo "VRAM: $total_vram_mb MB ($total_vram_gb GB $total_vram_mb_remain MB)"
|
||||
else
|
||||
echo "VRAM: Could not determine from lspci"
|
||||
fi
|
||||
# Check for AMD GPU via amdgpu sysfs
|
||||
if lspci -vnn 2>/dev/null | grep -qi 'amd\|ati'; then
|
||||
local amdgpu_info
|
||||
amdgpu_info=$(ls /sys/kernel/debug/amdgpu/ 2>/dev/null | head -1)
|
||||
if [[ -n "$amdgpu_info" ]]; then
|
||||
local vram_total
|
||||
vram_total=$(cat /sys/kernel/debug/amdgpu/${amdgpu_info}/vram_total 2>/dev/null || echo 0)
|
||||
if [[ "$vram_total" -gt 0 ]]; then
|
||||
local vram_gb=$((vram_total / 1024 / 1024 / 1024))
|
||||
local vram_mb=$((vram_total / 1024 / 1024))
|
||||
echo "AMD GPU VRAM: ${vram_gb}GB (${vram_mb}MB)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Total VRAM: $((total_vram_kb / 1024 / 1024))GB"
|
||||
echo "VRAM per GPU: $((vram_per_gpu_kb / 1024 / 1024))GB"
|
||||
echo "Num GPUs: $num_gpus"
|
||||
}
|
||||
|
||||
# ─── System RAM Detection ───
|
||||
detect_ram() {
|
||||
local total_kb=0
|
||||
local available_kb=0
|
||||
|
||||
if [[ -f /proc/meminfo ]]; then
|
||||
total_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
available_kb=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
echo "RAM: $((total_kb / 1024 / 1024))GB total, $((available_kb / 1024 / 1024))GB available"
|
||||
echo "$available_kb $total_kb"
|
||||
fi
|
||||
elif command -v sysctl &>/dev/null; then
|
||||
total_kb=$(sysctl -n hw.memsize 2>/dev/null | awk '{print $1 / 1024}')
|
||||
if [[ -n "$total_kb" && "$total_kb" -gt 0 ]]; then
|
||||
echo "RAM: $((total_kb / 1024))GB total"
|
||||
echo "$total_kb $total_kb" # Assume all available
|
||||
fi
|
||||
else
|
||||
echo "RAM: Could not detect"
|
||||
echo "0 0"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Model Context Window Detection ───
|
||||
detect_model_context() {
|
||||
local model_name="$1"
|
||||
local context_kb=0
|
||||
|
||||
# If model name provided, look it up
|
||||
if [[ -n "$model_name" ]]; then
|
||||
case "$model_name" in
|
||||
gpt-4o|gpt-4o-2024-05-13|gpt-4o-2024-08-06)
|
||||
context_kb=128000; echo "Model: $model_name"
|
||||
echo "Context window: 128k tokens"
|
||||
;;
|
||||
gpt-4o-mini|gpt-4o-mini-2024-07-18)
|
||||
context_kb=128000; echo "Model: $model_name"
|
||||
echo "Context window: 128k tokens"
|
||||
;;
|
||||
gpt-4-turbo|gpt-4-turbo-2024-04-09)
|
||||
context_kb=128000; echo "Model: $model_name"
|
||||
echo "Context window: 128k tokens"
|
||||
;;
|
||||
gpt-4|gpt-4-0125-preview|gpt-4-1106-preview)
|
||||
context_kb=128000; echo "Model: $model_name"
|
||||
echo "Context window: 128k tokens"
|
||||
;;
|
||||
claude-3-5-sonnet|claude-3-5-sonnet-20241022)
|
||||
context_kb=200000; echo "Model: $model_name"
|
||||
echo "Context window: 200k tokens"
|
||||
;;
|
||||
claude-3-5-haiku|claude-3-5-haiku-20241022)
|
||||
context_kb=200000; echo "Model: $model_name"
|
||||
echo "Context window: 200k tokens"
|
||||
;;
|
||||
claude-3-opus|claude-3-opus-20240229)
|
||||
context_kb=200000; echo "Model: $model_name"
|
||||
echo "Context window: 200k tokens"
|
||||
;;
|
||||
claude-3-sonnet|claude-3-sonnet-20240229)
|
||||
context_kb=200000; echo "Model: $model_name"
|
||||
echo "Context window: 200k tokens"
|
||||
;;
|
||||
claude-3-haiku|claude-3-haiku-20240307)
|
||||
context_kb=200000; echo "Model: $model_name"
|
||||
echo "Context window: 200k tokens"
|
||||
;;
|
||||
claude-2|claude-2.1)
|
||||
context_kb=200000; echo "Model: $model_name"
|
||||
echo "Context window: 200k tokens"
|
||||
;;
|
||||
*)
|
||||
echo "Model: $model_name (unknown context window)"
|
||||
echo "0"
|
||||
;;
|
||||
esac
|
||||
echo "$context_kb"
|
||||
return
|
||||
fi
|
||||
|
||||
# Try to detect from config.md (global framework model settings)
|
||||
local project_dir="${1:-.}"
|
||||
local config_md="${HOME}/.automaton/config.md"
|
||||
local model_from_config=""
|
||||
local override_context=""
|
||||
if [[ -f "$config_md" ]]; then
|
||||
# Check for model name
|
||||
model_from_config=$(grep -i "model:" "$config_md" 2>/dev/null | grep -v "#" | grep -v "model_context" | grep -v "override" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | tr -d '[:space:]')
|
||||
# Check for override context window
|
||||
override_context=$(grep -i "override context" "$config_md" 2>/dev/null | grep -v "#" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | tr -d '[:space:]')
|
||||
fi
|
||||
|
||||
# If config.md specifies a model, use it
|
||||
if [[ -n "$model_from_config" ]]; then
|
||||
echo "Found model in config.md: $model_from_config"
|
||||
# Use override context window if specified in config.md
|
||||
if [[ -n "$override_context" && "$override_context" != "auto" ]]; then
|
||||
echo "Using override context window from config.md: $override_context"
|
||||
local override_kb
|
||||
override_kb=$(echo "$override_context" | sed 's/[kK]$//' | grep -oP '[0-9]+' || true)
|
||||
if [[ -n "$override_kb" ]]; then
|
||||
echo "Context window: ${override_context} tokens (override)"
|
||||
echo "$((override_kb * 1000))"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
detect_model_context "$model_from_config"
|
||||
return
|
||||
fi
|
||||
|
||||
# Try to detect from .agent.md (project-level model override)
|
||||
local agent_md="${project_dir}/.automaton/.agent.md"
|
||||
if [[ -f "$agent_md" ]]; then
|
||||
local model_line
|
||||
model_line=$(grep -i "model" "$agent_md" 2>/dev/null | grep -v "#" | grep -v "target" | grep -v "headroom" | grep -v "peak" | grep -v "Auto-detect" | head -1)
|
||||
if [[ -n "$model_line" ]]; then
|
||||
echo "Found model in .agent.md: $model_line"
|
||||
# Extract model name from the line
|
||||
local model
|
||||
model=$(echo "$model_line" | sed -E 's/.*[:=[:space:]]+//i' | tr -d '[:space:]')
|
||||
if [[ -n "$model" ]]; then
|
||||
# Use override context window if specified in config.md
|
||||
if [[ -n "$override_context" && "$override_context" != "auto" ]]; then
|
||||
echo "Using override context window from config.md: $override_context"
|
||||
# Convert override_context to kb (e.g., 128k -> 128000, 200k -> 200000)
|
||||
local override_kb
|
||||
override_kb=$(echo "$override_context" | sed 's/[kK]$//' | grep -oP '[0-9]+' || true)
|
||||
if [[ -n "$override_kb" ]]; then
|
||||
echo "Context window: ${override_context} tokens (override)"
|
||||
echo "$((override_kb * 1000))"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
detect_model_context "$model"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try to detect from common API config files
|
||||
local config_files=(
|
||||
".env"
|
||||
".env.local"
|
||||
"config.yaml"
|
||||
"config.yml"
|
||||
"config.json"
|
||||
"settings.yaml"
|
||||
".automaton/config.yaml"
|
||||
".automaton/config.json"
|
||||
)
|
||||
|
||||
for config_file in "${config_files[@]}"; do
|
||||
local abs_file=""
|
||||
for candidate in "${project_dir}/${config_file}" "${project_dir}/.automaton/${config_file}"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
abs_file="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$abs_file" ]]; then
|
||||
local model
|
||||
model=$(grep -i "model" "$abs_file" 2>/dev/null | grep -v "#" | grep -v "context" | grep -v "max_tokens" | grep -v "temperature" | grep -v "stream" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | tr -d '[:space:]')
|
||||
if [[ -n "$model" ]]; then
|
||||
echo "Found model in $abs_file: $model"
|
||||
# Use override context window if specified in config.md
|
||||
if [[ -n "$override_context" && "$override_context" != "auto" ]]; then
|
||||
echo "Using override context window from config.md: $override_context"
|
||||
local override_kb
|
||||
override_kb=$(echo "$override_context" | sed 's/[kK]$//' | grep -oP '[0-9]+' || true)
|
||||
if [[ -n "$override_kb" ]]; then
|
||||
echo "Context window: ${override_context} tokens (override)"
|
||||
echo "$((override_kb * 1000))"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
detect_model_context "$model"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Model: Unknown (could not detect from .agent.md or config files)"
|
||||
echo "0"
|
||||
}
|
||||
|
||||
# ─── Agent Framework Overhead Calculation ───
|
||||
calculate_overhead() {
|
||||
local project_dir="${1:-.}"
|
||||
local overhead_tokens=0
|
||||
|
||||
# Count tokens for the framework files that are loaded during orchestration
|
||||
# These are the files loaded during the most common phase (orchestration):
|
||||
# .agent.md + .rules.md + workflow.md + orchestrate.md
|
||||
# Note: other phase files (decompose.md, implement.md, etc.) are only loaded during
|
||||
# their specific phases, so they don't contribute to the peak context during orchestration.
|
||||
local framework_files=(
|
||||
"${project_dir}/.automaton/.agent.md"
|
||||
"${project_dir}/.automaton/.rules.md"
|
||||
"${project_dir}/.automaton/prompts/workflow.md"
|
||||
"${project_dir}/.automaton/prompts/orchestrate.md"
|
||||
)
|
||||
# Fallback: check home directory if project dir doesn't have framework
|
||||
if [[ ! -f "${project_dir}/.automaton/.agent.md" ]]; then
|
||||
framework_files=(
|
||||
"${HOME}/.automaton/.agent.md"
|
||||
"${HOME}/.automaton/.rules.md"
|
||||
"${HOME}/.automaton/prompts/workflow.md"
|
||||
"${HOME}/.automaton/prompts/orchestrate.md"
|
||||
)
|
||||
fi
|
||||
|
||||
for file in "${framework_files[@]}"; do
|
||||
if [[ -f "$file" ]]; then
|
||||
# Rough estimate: 1 token ≈ 4 characters (English text)
|
||||
local chars
|
||||
chars=$(wc -c < "$file" 2>/dev/null || echo 0)
|
||||
local tokens=$((chars / 4))
|
||||
overhead_tokens=$((overhead_tokens + tokens))
|
||||
echo " ${file##*/}: ~${tokens} tokens"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Framework overhead: ~${overhead_tokens} tokens"
|
||||
echo "$overhead_tokens"
|
||||
}
|
||||
|
||||
# ─── Recommendation Engine ───
|
||||
recommend_context() {
|
||||
local gpu_vram_gb="$1"
|
||||
local ram_gb="$2"
|
||||
local model_context_kb="$3"
|
||||
local overhead_tokens="$4"
|
||||
|
||||
# Read VRAM config from config.md if it exists
|
||||
local config_md="${HOME}/.automaton/config.md"
|
||||
local auto_detect="Yes"
|
||||
local target_context_kb=0
|
||||
local override_headroom=25
|
||||
local override_max_peak_kb=0
|
||||
if [[ -f "$config_md" ]]; then
|
||||
auto_detect=$(grep -i "auto-detect:" "$config_md" 2>/dev/null | grep -v "#" | grep -i "vram" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | tr -d '[:space:]' || true)
|
||||
target_context_kb=$(grep -i "target.*context" "$config_md" 2>/dev/null | grep -v "#" | grep -i "vram" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | grep -oP '\d+' || true)
|
||||
override_headroom=$(grep -i "headroom" "$config_md" 2>/dev/null | grep -v "#" | grep -i "vram" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | grep -oP '\d+' || true)
|
||||
override_max_peak_kb=$(grep -i "max peak" "$config_md" 2>/dev/null | grep -v "#" | grep -i "vram" | head -1 | sed -E 's/.*[:=[:space:]]+//i' | grep -oP '\d+' || true)
|
||||
fi
|
||||
|
||||
# If auto-detect is disabled, use the manually specified values
|
||||
if [[ -n "$auto_detect" && "$auto_detect" == "No" ]]; then
|
||||
if [[ -n "$target_context_kb" ]]; then
|
||||
local max_peak_kb=${override_max_peak_kb:-0}
|
||||
if [[ $max_peak_kb -eq 0 && $headroom_pct -gt 0 ]]; then
|
||||
max_peak_kb=$((target_context_kb * (100 - headroom_pct) / 100))
|
||||
fi
|
||||
echo "$headroom_pct"
|
||||
echo "$target_context_kb"
|
||||
echo "$max_peak_kb"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
local recommended_kb=0
|
||||
local headroom_pct=${override_headroom:-25} # Use override from config.md, or default to 25%
|
||||
|
||||
# Output headroom_pct first (for parent to read)
|
||||
# Then output recommended_kb
|
||||
# Then output max_peak_kb (only for manual mode)
|
||||
echo "$headroom_pct"
|
||||
|
||||
# Recommendation logic:
|
||||
# 1. If GPU VRAM >= 4GB: use VRAM (practical for local inference)
|
||||
# 2. If model context window is available: use it (for API inference)
|
||||
# 3. If GPU VRAM < 4GB but > 0: use RAM (VRAM too small for local inference)
|
||||
# 4. If no GPU VRAM and no model: use RAM as fallback
|
||||
|
||||
# If GPU VRAM >= 4GB, base it on VRAM
|
||||
if [[ $gpu_vram_gb -ge 4 ]]; then
|
||||
# Rule of thumb: 1GB VRAM ≈ 4k tokens for local LLMs
|
||||
# But we need to leave room for the model itself
|
||||
# For a model, each ~8k context tokens takes about ~3-5MB of GPU VRAM
|
||||
# So VRAM available for context = VRAM - model size - agent overhead
|
||||
# Conservative: 1GB VRAM ≈ 2k context tokens
|
||||
local vram_context_kb=$((gpu_vram_gb * 2000))
|
||||
|
||||
# Leave headroom for the model itself and agent overhead
|
||||
recommended_kb=$((vram_context_kb * (100 - headroom_pct) / 100))
|
||||
# If model context window is available, use it (for API inference)
|
||||
elif [[ $model_context_kb -gt 0 ]]; then
|
||||
# For API-based, we're limited by the model's context window
|
||||
# But we don't want to use the full window due to overhead
|
||||
recommended_kb=$((model_context_kb * (100 - headroom_pct) / 100))
|
||||
# Fallback: use RAM to estimate
|
||||
else
|
||||
# Moderate estimate for low-VRAM systems where VRAM is too small for local inference
|
||||
# but RAM is available. Use 0.75k tokens per GB of RAM as a moderate estimate.
|
||||
# This balances between being too conservative (0.5k/GB) and too generous (1k/GB).
|
||||
local ram_context_kb=$((ram_gb * 750))
|
||||
recommended_kb=$((ram_context_kb * (100 - headroom_pct) / 100))
|
||||
fi
|
||||
|
||||
# Subtract framework overhead
|
||||
local net_kb=$((recommended_kb - overhead_tokens))
|
||||
if [[ $net_kb -lt 0 ]]; then
|
||||
net_kb=0
|
||||
fi
|
||||
|
||||
# Output: headroom_pct, recommended_kb
|
||||
echo "$net_kb"
|
||||
}
|
||||
|
||||
# ─── Main ───
|
||||
main() {
|
||||
local model_name=""
|
||||
local project_dir="."
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--model|-m)
|
||||
model_name="$2"
|
||||
shift 2
|
||||
;;
|
||||
--project|-p)
|
||||
project_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
# Could be model name as first argument
|
||||
if [[ -z "$model_name" ]]; then
|
||||
model_name="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== VRAM / Context Detection ==="
|
||||
echo ""
|
||||
|
||||
# Detect GPU VRAM
|
||||
echo "--- GPU VRAM ---"
|
||||
detect_gpu_vram
|
||||
local gpu_vram_kb=0
|
||||
local gpu_vram_gb=0
|
||||
# Use timeout to avoid hanging
|
||||
gpu_vram_kb=$(timeout 5 nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ' || true)
|
||||
# Validate that vram_kb is a positive number
|
||||
if [[ -z "$gpu_vram_kb" || ! "$gpu_vram_kb" =~ ^[0-9]+$ || "$gpu_vram_kb" -le 0 ]]; then
|
||||
gpu_vram_kb=0
|
||||
fi
|
||||
# If nvidia-smi didn't work, try to detect from lspci (AMD GPUs)
|
||||
if [[ $gpu_vram_kb -eq 0 ]]; then
|
||||
echo " nvidia-smi failed, checking lspci for AMD GPU VRAM..."
|
||||
local total_vram_mb=0
|
||||
while IFS= read -r line; do
|
||||
local size_num
|
||||
size_num=$(echo "$line" | grep -oE '[0-9]+(M|G|K)' | head -1 || true)
|
||||
if [[ -n "$size_num" ]]; then
|
||||
local size_val
|
||||
size_val=$(echo "$size_num" | grep -oE '[0-9]+')
|
||||
local size_unit
|
||||
size_unit=$(echo "$size_num" | grep -oE '(M|G|K)')
|
||||
if [[ -n "$size_val" && -n "$size_unit" ]]; then
|
||||
case "$size_unit" in
|
||||
M) total_vram_mb=$((total_vram_mb + size_val)) ;;
|
||||
G) total_vram_mb=$((total_vram_mb + size_val * 1024)) ;;
|
||||
K) total_vram_mb=$((total_vram_mb + size_val / 1024)) ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
done < <(lspci -vnn 2>/dev/null | grep -i -A 15 -E 'VGA|3D|Display' | grep -i 'Memory at')
|
||||
if [[ $total_vram_mb -gt 0 ]]; then
|
||||
gpu_vram_kb=$((total_vram_mb * 1024))
|
||||
gpu_vram_gb=$((total_vram_mb / 1024))
|
||||
echo " AMD GPU VRAM from lspci: ${gpu_vram_gb}GB ($total_vram_mb MB)"
|
||||
else
|
||||
echo " No VRAM found from lspci"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Detect RAM
|
||||
echo "--- RAM ---"
|
||||
detect_ram
|
||||
local ram_kb
|
||||
ram_kb=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}' || echo 0)
|
||||
local ram_gb=$((ram_kb / 1024 / 1024))
|
||||
echo ""
|
||||
|
||||
# Detect model context window
|
||||
echo "--- Model Context Window ---"
|
||||
detect_model_context "$model_name"
|
||||
local model_context_kb
|
||||
model_context_kb=$(detect_model_context "$model_name" | tail -1)
|
||||
echo ""
|
||||
|
||||
# Calculate framework overhead
|
||||
echo "--- Framework Overhead ---"
|
||||
calculate_overhead "$project_dir"
|
||||
local overhead_tokens
|
||||
overhead_tokens=$(calculate_overhead "$project_dir" | tail -1)
|
||||
echo ""
|
||||
|
||||
# Recommend context window (also outputs headroom_pct and recommended_kb)
|
||||
echo "--- Recommendation ---"
|
||||
local recommendation_output
|
||||
recommendation_output=$(recommend_context "$gpu_vram_gb" "$ram_gb" "$model_context_kb" "$overhead_tokens")
|
||||
local line_count
|
||||
line_count=$(echo "$recommendation_output" | wc -l)
|
||||
local recommended_kb
|
||||
local headroom_pct
|
||||
local max_peak_kb
|
||||
if [[ $line_count -ge 3 ]]; then
|
||||
# Manual mode: outputs headroom_pct, recommended_kb, max_peak_kb
|
||||
headroom_pct=$(echo "$recommendation_output" | head -1 | tr -d '[:space:]')
|
||||
recommended_kb=$(echo "$recommendation_output" | sed -n '2p' | tr -d '[:space:]')
|
||||
max_peak_kb=$(echo "$recommendation_output" | tail -1 | tr -d '[:space:]')
|
||||
else
|
||||
# Auto-detect mode: outputs headroom_pct, recommended_kb
|
||||
headroom_pct=$(echo "$recommendation_output" | head -1 | tr -d '[:space:]')
|
||||
recommended_kb=$(echo "$recommendation_output" | tail -1 | tr -d '[:space:]')
|
||||
# Calculate max peak context based on headroom
|
||||
max_peak_kb=$((recommended_kb * (100 - headroom_pct) / 100))
|
||||
fi
|
||||
|
||||
# Convert to human-readable
|
||||
local recommended_k
|
||||
if [[ $recommended_kb -gt 0 ]]; then
|
||||
recommended_k=$((recommended_kb / 1000))
|
||||
else
|
||||
recommended_k=8 # Default fallback
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Recommended Configuration ==="
|
||||
echo "Target context: ${recommended_k}k tokens"
|
||||
echo "Headroom: ${headroom_pct}%"
|
||||
echo "Max peak context per sub-task: $((recommended_k * (100 - headroom_pct) / 100))k tokens"
|
||||
|
||||
# Output as JSON for programmatic use
|
||||
echo ""
|
||||
echo "=== JSON Output ==="
|
||||
cat <<EOF
|
||||
{
|
||||
"gpu_vram_gb": $gpu_vram_gb,
|
||||
"ram_gb": $ram_gb,
|
||||
"model_context_kb": $model_context_kb,
|
||||
"framework_overhead_tokens": $overhead_tokens,
|
||||
"recommended_kb": $recommended_kb,
|
||||
"recommended_k": $recommended_k,
|
||||
"headroom": 0.25,
|
||||
"max_peak_context_kb": $max_peak_kb
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user