atrade/src/main/kotlin/service/LlamaServerManager.kt
2026-04-08 08:23:39 +09:00

228 lines
9.8 KiB
Kotlin

package service
import Defines.DETAILLOG
import Defines.EMBEDDING_PORT
import Defines.LLM_PORT
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import network.RagService
import util.HardwareDetector
import util.NetworkPortDiagnostic
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.util.concurrent.ConcurrentHashMap
object LlamaServerManager {
// 포트별로 프로세스를 관리합니다.
private val processes = ConcurrentHashMap<Int, Process>()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
killZombieProcesses()
Runtime.getRuntime().addShutdownHook(Thread {
stopAll()
})
}
// OS 시스템 명령어를 이용해 찌꺼기 프로세스를 이름으로 찾아 강제 종료하는 함수
private fun killZombieProcesses() {
try {
val os = System.getProperty("os.name").lowercase()
if (os.contains("win")) {
// 윈도우: taskkill 명령어로 llama-server.exe 강제 종료 (/F: 강제, /T: 트리거된 자식까지)
ProcessBuilder("cmd", "/c", "taskkill /F /IM llama-server.exe /T")
.redirectErrorStream(true)
.start()
.waitFor()
println("🧹 [System] 이전 llama-server 좀비 프로세스 정리 완료 (Windows)")
} else {
// 맥/리눅스: pkill 사용
ProcessBuilder("pkill", "-f", "llama-server")
.redirectErrorStream(true)
.start()
.waitFor()
println("🧹 [System] 이전 llama-server 좀비 프로세스 정리 완료 (Mac/Linux)")
}
} catch (e: Exception) {
// 실행 중인 프로세스가 없어서 에러가 나도 조용히 무시합니다.
}
}
fun checkPortStatus(port: Int): String {
return try {
// netstat 명령어로 해당 포트를 점유 중인 프로세스 확인
val process = Runtime.getRuntime().exec("cmd /c netstat -ano | findstr :$port")
val reader = process.inputStream.bufferedReader()
val result = reader.readText()
if (result.contains("LISTENING")) {
val pid = result.trim().split(Regex("\\s+")).last()
"✅ 포트 $port 상태: 사용 중 (PID: $pid - 정상 대기 중)"
} else {
"⚠️ 포트 $port 상태: 리스닝 상태가 아님 (서버 미구동 또는 차단 가능성)"
}
} catch (e: Exception) {
"❌ 포트 점검 실패: ${e.message}"
}
}
fun startServer(binPath: String, modelPath: String, port: Int) {
val os = System.getProperty("os.name").lowercase()
val arch = System.getProperty("os.arch").lowercase()
val isWin = os.contains("win")
val isMacArm = os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64"))
val canUsePort = if (isWin) NetworkPortDiagnostic.testPortAvailability(port) else true
if (canUsePort) {
if (processes.containsKey(port) || modelPath.isBlank()) return
val cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일
val hasGpu = HardwareDetector.hasNvidiaGpu()
val ratio = if (isWin) 0.5 else 0.7
val optimalThreads = if(isWin)(cpuCores * ratio).toInt().coerceIn(4, 12) else (cpuCores * ratio).toInt().coerceIn(4, 16)
var optimalGpuLayers = 99 //if ((isWin && hasGpu) || isMacArm) 99 else 4
// if(HardwareDetector.getCpuName().contains("i7")) {
// optimalGpuLayers = 0
// }
println("🖥️ OS: $os / Arch: $arch")
println("⚙️ 할당 스레드: $optimalThreads (Core: $cpuCores, Ratio: $ratio)")
println("🚀 GPU 레이어: $optimalGpuLayers (NVIDIA/MacArm: ${if(optimalGpuLayers == 99) "YES" else "NO"})")
val command = mutableListOf(
binPath,
"-m", modelPath,
"--port", port.toString(),
"-c", if (port == EMBEDDING_PORT) "2048" else "8192",
"-ngl", optimalGpuLayers.toString(),
"-t", optimalThreads.toString(),
"--embedding",
"--mlock", // RAM 고정으로 스왑 방지
"--no-mmap",
)
if (port != EMBEDDING_PORT) { // 텍스트 생성용 모델에만 적용
command.addAll(listOf(
"-b", "512", // Batch size (토큰 병렬 처리량 제한으로 연산 안정화)
"--threads-batch", optimalThreads.toString(),
"-fa","on", // Flash Attention 활성화 (메모리 절약 및 긴 컨텍스트 연산 안정성 증가)
"--cont-batching"
))
}
if (isWin) {
command.addAll(listOf(
"--numa", "distribute"
))
}
scope.launch {
try {
val pb = ProcessBuilder(command)
// 2. 윈도우 Vulkan 환경 변수 설정
if (isWin ) {
val env = pb.environment()
// 특정 GPU 선택 (내장 GPU가 여러 개일 경우)
env["GGML_VULKAN_DEVICE"] = "0"
env["GGML_VULKAN_MAX_NODES"] = "1"
// DLL 로드 경로 강제 지정 (bin 폴더 내 dll 참조)
val libraryPath = File(binPath).parentFile.absolutePath
val currentPath = System.getenv("PATH") ?: ""
env["PATH"] = "$libraryPath;$currentPath"
println("🔧 [Vulkan] 환경 변수 설정 완료: $libraryPath")
}
pb.redirectErrorStream(true)
File(binPath).setExecutable(true)
val process = pb.start()
processes[port] = process
println("✅ AI 서버 시작 시도 (Port: $port, Model: ${File(modelPath).name})")
if (isWin) {
delay(3000)
val status = checkPortStatus(port)
println(status) // 콘솔 로그
TradingLogStore.addAnalyzer("System", "Port:$port", status, status.contains(""))
}
Thread {
process.errorStream.bufferedReader().use { reader ->
reader.lines().forEach { line -> println("[SERVER ERROR] $line") }
}
}.start()
val reader = BufferedReader(InputStreamReader(process.inputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
// 로그 출력 (디버깅용)
if (DETAILLOG) println("[Server $port] $line")
if (line?.contains("server is listening") == true) {
println("🚀 AI 서버 준비 완료 (Port: $port)")
if (port == LLM_PORT){
AutoTradingManager.llmAnalyser = true
}
if (port == EMBEDDING_PORT){
AutoTradingManager.llmNews = true
RagService.active()
}
if (processes.size > 1) {
println("[Cache] ${processes.size}")
}
}
}
} catch (e: Exception) {
println("❌ AI 서버 실행 실패 (Port: $port): ${e.message}")
processes.remove(port)
}
}
} else {
println("🚨 포트 $port 가 보안 정책에 의해 막혀있어 서버 기동을 중단합니다.")
TradingLogStore.addAnalyzer("System", "Port:$port", "보안 정책에 의한 포트 차단 감지", false)
}
}
fun stopAll(): Boolean {
var allStopped = true // 모든 프로세스 종료 여부를 추적하는 플래그
processes.forEach { (port, process) ->
try {
process.destroy() // 1차: 부드러운 종료 시도
// 2차: 최대 3초 대기 후 종료되지 않으면 강제 종료
if (!process.waitFor(3, java.util.concurrent.TimeUnit.SECONDS)) {
process.destroyForcibly() // 강제 사살
// 강제 종료 후에도 프로세스가 살아있는지 최종 확인
if (process.isAlive) {
println("❌ [Server $port] 강제 종료 명령 후에도 프로세스가 살아있습니다.")
allStopped = false
} else {
println("⚠️ [Server $port] 응답이 없어 강제 종료되었습니다.")
}
} else {
println("🛑 [Server $port] 정상 종료되었습니다.")
}
} catch (e: Exception) {
println("❌ [Server $port] 종료 중 오류: ${e.message}")
allStopped = false
}
}
if (allStopped) {
processes.clear() // 모든 프로세스가 성공적으로 종료되거나 리스트에서 제거될 준비가 된 경우
}
return allStopped
}
}