// File: backend/src/modules/ai/services/vram-monitor.service.ts // Change Log: // - 2026-06-11: Initial creation of VramMonitorService to monitor VRAM headroom from Ollama /api/ps // - 2026-06-11: เพิ่มการคำนวณ mainModelVramMb ใน getVramHeadroom // - 2026-06-11: เพิ่ม getVramStatus และ invalidateCache เพื่อความเข้ากันได้กับส่วนอื่น import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import { VramHeadroom } from '../interfaces/execution-policy.interface'; /** * ผลลัพธ์ VRAM status สำหรับส่วนบริการภายนอก * ผลลัพธ์นี้มีวัตถุประสงค์เพื่อรักษาความเข้ากันได้ย้อนหลัง (Backward Compatibility) */ export interface VramStatus { totalVramMb: number; usedVramMb: number; freeVramMb: number; loadedModels: Array<{ modelId: string; modelName: string; vramUsageMB: number; }>; hasCapacity: boolean; } @Injectable() export class VramMonitorService { private readonly logger = new Logger(VramMonitorService.name); private readonly ollamaUrl: string; private readonly totalVramMb: number; constructor(private readonly configService: ConfigService) { this.ollamaUrl = this.configService.get( 'OLLAMA_URL', this.configService.get( 'AI_HOST_URL', 'http://192.168.10.100:11434' ) ); this.totalVramMb = this.configService.get( 'GPU_TOTAL_VRAM_MB', 16384 // Default to 16GB (RTX 5060 Ti) ); } /** * ดึงสถานะ VRAM headroom จาก Ollama /api/ps * ถ้าล้มเหลวจะคืนค่าด้วย safe default (available = 0) */ async getVramHeadroom(): Promise { try { const response = await axios.get<{ models?: Array<{ name: string; size_vram: number; }>; }>(`${this.ollamaUrl}/api/ps`, { timeout: 3000 }); const models = response.data?.models ?? []; let totalUsedBytes = 0; let mainModelUsedBytes = 0; for (const model of models) { totalUsedBytes += model.size_vram || 0; if ( model.name.includes('np-dms-ai') || model.name.includes('typhoon2.5-np-dms') ) { mainModelUsedBytes += model.size_vram || 0; } } const usedMb = Math.round(totalUsedBytes / (1024 * 1024)); const availableMb = Math.max(0, this.totalVramMb - usedMb); const mainModelVramMb = Math.round(mainModelUsedBytes / (1024 * 1024)); return { totalMb: this.totalVramMb, usedMb, availableMb, querySuccess: true, mainModelVramMb, }; } catch (err: unknown) { this.logger.warn( `Failed to query Ollama /api/ps: ${err instanceof Error ? err.message : String(err)}` ); // เปลี่ยนจาก pessimistic (assume all VRAM used) เป็น optimistic (assume no VRAM used) // เพื่อป้องกัน false positive OOM Guard เมื่อ query ล้มเหลวแต่ไม่มี model load จริง return { totalMb: this.totalVramMb, usedMb: 0, // สมมติว่าไม่มี model load เมื่อ query ล้มเหลว availableMb: this.totalVramMb, querySuccess: false, mainModelVramMb: 0, }; } } /** * ดึงสถานะ VRAM ปัจจุบันของระบบ * เพื่อความเข้ากันได้ย้อนหลังกับ endpoint vram/status */ async getVramStatus(minRequiredMb = 4000): Promise { try { const response = await axios.get<{ models?: Array<{ name: string; size_vram: number; }>; }>(`${this.ollamaUrl}/api/ps`, { timeout: 3000 }); const models = response.data?.models ?? []; const loadedModels = models.map((m) => ({ modelId: m.name, modelName: m.name, vramUsageMB: Math.round((m.size_vram || 0) / (1024 * 1024)), })); const headroom = await this.getVramHeadroom(); return { totalVramMb: headroom.totalMb, usedVramMb: headroom.usedMb, freeVramMb: headroom.availableMb, loadedModels, hasCapacity: headroom.availableMb >= minRequiredMb, }; } catch (err: unknown) { this.logger.warn( `Failed to get VRAM status: ${err instanceof Error ? err.message : String(err)}` ); return { totalVramMb: this.totalVramMb, usedVramMb: this.totalVramMb, freeVramMb: 0, loadedModels: [], hasCapacity: false, }; } } /** * ตรวจสอบว่า VRAM เพียงพอสำหรับความต้องการโหลดโมเดลหรือไม่ */ async hasVramCapacity(requiredMb: number): Promise { const headroom = await this.getVramHeadroom(); return headroom.availableMb >= requiredMb; } /** * ล้าง cache VRAM (ไม่มี cache แล้วในระบบใหม่ แต่เก็บไว้เพื่อรองรับการเรียกใช้เดิม) */ async invalidateCache(): Promise { await Promise.resolve(); this.logger.log('VRAM cache invalidation requested (no-op in new policy)'); } }