Multi-GPU Collection System: - Add modular GPU collector architecture in collectors/gpu/ - Support AMD (amdgpu), NVIDIA (nvidia-smi), and Intel (i915/xe) GPUs - GPU Manager auto-detects and aggregates all vendor collectors - Backward-compatible JSON output for existing frontend Operational Modes: - Standalone mode (default): single-host monitoring, no database - Server mode: multi-device with database, auth, agents (WIP) - Agent mode: lightweight reporter to central server (WIP) - Mode selection via TYTO_MODE env var or config.yaml Configuration Updates: - Add server config (gRPC port, mTLS settings, registration) - Add agent config (ID, server URL, TLS certificates) - Add database config (SQLite/PostgreSQL support) - Support TYTO_* prefixed environment variables 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
194 lines
4.7 KiB
Go
194 lines
4.7 KiB
Go
package gpu
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"tyto/internal/models"
|
|
)
|
|
|
|
func TestAMDCollector(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
sysPath := filepath.Join(tmpDir, "sys")
|
|
|
|
// Create mock AMD GPU sysfs structure
|
|
gpuPath := filepath.Join(sysPath, "class/drm/card0/device")
|
|
if err := os.MkdirAll(gpuPath, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create driver symlink (required for AMD GPU detection)
|
|
driverTarget := filepath.Join(tmpDir, "drivers/amdgpu")
|
|
if err := os.MkdirAll(driverTarget, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(driverTarget, filepath.Join(gpuPath, "driver")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// GPU utilization
|
|
if err := os.WriteFile(filepath.Join(gpuPath, "gpu_busy_percent"), []byte("75\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// VRAM
|
|
if err := os.WriteFile(filepath.Join(gpuPath, "mem_info_vram_used"), []byte("4294967296\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(gpuPath, "mem_info_vram_total"), []byte("17179869184\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Clock frequencies
|
|
sclk := "0: 500Mhz\n1: 800Mhz\n2: 1200Mhz *\n"
|
|
if err := os.WriteFile(filepath.Join(gpuPath, "pp_dpm_sclk"), []byte(sclk), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mclk := "0: 400Mhz\n1: 875Mhz *\n"
|
|
if err := os.WriteFile(filepath.Join(gpuPath, "pp_dpm_mclk"), []byte(mclk), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create hwmon for temperature, power, fan
|
|
hwmonPath := filepath.Join(gpuPath, "hwmon/hwmon5")
|
|
if err := os.MkdirAll(hwmonPath, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Temperature (65°C in millidegrees)
|
|
if err := os.WriteFile(filepath.Join(hwmonPath, "temp1_input"), []byte("65000\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Power (150W in microwatts)
|
|
if err := os.WriteFile(filepath.Join(hwmonPath, "power1_average"), []byte("150000000\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Fan RPM
|
|
if err := os.WriteFile(filepath.Join(hwmonPath, "fan1_input"), []byte("1500\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
collector := NewAMDCollector(sysPath)
|
|
count := collector.Detect()
|
|
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 GPU, got %d", count)
|
|
}
|
|
|
|
gpus, err := collector.Collect()
|
|
if err != nil {
|
|
t.Fatalf("Collect failed: %v", err)
|
|
}
|
|
|
|
if len(gpus) != 1 {
|
|
t.Fatalf("Expected 1 GPU result, got %d", len(gpus))
|
|
}
|
|
|
|
gpu := gpus[0]
|
|
|
|
if gpu.Vendor != models.GPUVendorAMD {
|
|
t.Errorf("Expected vendor AMD, got %s", gpu.Vendor)
|
|
}
|
|
|
|
if gpu.Utilization != 75 {
|
|
t.Errorf("Expected utilization 75, got %d", gpu.Utilization)
|
|
}
|
|
|
|
if gpu.MemoryUsed != 4294967296 {
|
|
t.Errorf("Expected VRAM used 4294967296, got %d", gpu.MemoryUsed)
|
|
}
|
|
|
|
if gpu.MemoryTotal != 17179869184 {
|
|
t.Errorf("Expected VRAM total 17179869184, got %d", gpu.MemoryTotal)
|
|
}
|
|
|
|
if gpu.ClockCore != 1200 {
|
|
t.Errorf("Expected GPU clock 1200, got %d", gpu.ClockCore)
|
|
}
|
|
|
|
if gpu.ClockMemory != 875 {
|
|
t.Errorf("Expected memory clock 875, got %d", gpu.ClockMemory)
|
|
}
|
|
|
|
if gpu.Temperature != 65.0 {
|
|
t.Errorf("Expected temperature 65.0, got %f", gpu.Temperature)
|
|
}
|
|
|
|
if gpu.PowerWatts != 150.0 {
|
|
t.Errorf("Expected power 150.0W, got %f", gpu.PowerWatts)
|
|
}
|
|
|
|
if gpu.FanRPM != 1500 {
|
|
t.Errorf("Expected fan 1500 RPM, got %d", gpu.FanRPM)
|
|
}
|
|
}
|
|
|
|
func TestAMDCollector_NoGPU(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
collector := NewAMDCollector(tmpDir)
|
|
|
|
count := collector.Detect()
|
|
if count != 0 {
|
|
t.Errorf("Expected 0 GPUs, got %d", count)
|
|
}
|
|
|
|
gpus, err := collector.Collect()
|
|
if err != nil {
|
|
t.Fatalf("Collect failed: %v", err)
|
|
}
|
|
|
|
if len(gpus) != 0 {
|
|
t.Errorf("Expected 0 GPU results, got %d", len(gpus))
|
|
}
|
|
}
|
|
|
|
func TestAMDCollector_MultipleGPUs(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
sysPath := filepath.Join(tmpDir, "sys")
|
|
|
|
// Create two AMD GPUs
|
|
for i := 0; i < 2; i++ {
|
|
gpuPath := filepath.Join(sysPath, "class/drm", "card"+string(rune('0'+i)), "device")
|
|
if err := os.MkdirAll(gpuPath, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
driverTarget := filepath.Join(tmpDir, "drivers/amdgpu")
|
|
if err := os.MkdirAll(driverTarget, 0755); err != nil {
|
|
// Already exists, ignore
|
|
}
|
|
// Create symlink only if it doesn't exist
|
|
driverLink := filepath.Join(gpuPath, "driver")
|
|
if _, err := os.Lstat(driverLink); os.IsNotExist(err) {
|
|
if err := os.Symlink(driverTarget, driverLink); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Minimal GPU data
|
|
if err := os.WriteFile(filepath.Join(gpuPath, "gpu_busy_percent"), []byte("50\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
collector := NewAMDCollector(sysPath)
|
|
count := collector.Detect()
|
|
|
|
if count != 2 {
|
|
t.Errorf("Expected 2 GPUs, got %d", count)
|
|
}
|
|
|
|
gpus, err := collector.Collect()
|
|
if err != nil {
|
|
t.Fatalf("Collect failed: %v", err)
|
|
}
|
|
|
|
if len(gpus) != 2 {
|
|
t.Errorf("Expected 2 GPU results, got %d", len(gpus))
|
|
}
|
|
}
|