From 0479d5777a74d37026944830e0ea0b229270fb46 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 27 Mar 2026 13:38:05 +0900 Subject: [PATCH] ... --- src/main/kotlin/network/RagService.kt | 55 ++++++++++++++----- src/main/kotlin/service/DynamicNewsScraper.kt | 14 ++++- src/main/kotlin/service/LlamaServerManager.kt | 38 +++++++++---- src/main/kotlin/util/HardwareDetector.kt | 21 +++++++ 4 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/util/HardwareDetector.kt diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index bede04e..416a52c 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -174,37 +174,53 @@ object RagService { } } - suspend fun processStock(currentPrice : Double , technicalAnalyzer: TechnicalAnalyzer,stockName: String,stockCode: String,result : TradingDecisionCallback) { - // 1. 10분간의 데이터 가져오기 (API 호출) + suspend fun processStock(currentPrice: Double, technicalAnalyzer: TechnicalAnalyzer, stockName: String, stockCode: String, result: TradingDecisionCallback) { + val totalStartTime = System.currentTimeMillis() // 전체 시작 시간 + coroutineScope { try { - var tradingDecision: TradingDecision = TradingDecision() + var tradingDecision = TradingDecision() tradingDecision.stockCode = stockCode tradingDecision.analyzer = technicalAnalyzer tradingDecision.currentPrice = currentPrice + var corpInfo = DartCodeManager.getCorpCode(stockCode) corpInfo?.stockName = stockName tradingDecision.stockName = stockName tradingDecision.corpName = corpInfo?.cName ?: "" + + // 1. 재무 데이터 가져오기 시간 측정 + val financialStartTime = System.currentTimeMillis() val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") } - tradingDecision.financialData = financialDataDeferred.await() - val financialStmt = FinancialMapper.mapRawTextToStatement(tradingDecision.financialData ?: "") + val financialDuration = System.currentTimeMillis() - financialStartTime + println("⏱️ [$stockName] 재무 분석 소요: ${financialDuration}ms") + if (FinancialAnalyzer.isSafetyBeltMet(financialStmt)) { + // 2. 뉴스 스크래핑 및 학습 시간 측정 + val newsIngestStartTime = System.currentTimeMillis() corpInfo?.let { try { NewsService.fetchAndIngestNews(it) } catch (e: Exception) {} } + val newsIngestDuration = System.currentTimeMillis() - newsIngestStartTime + println("⏱️ [$stockName] 뉴스 수집/인덱싱 소요: ${newsIngestDuration}ms") + // 3. 기술적 지표 계산 시간 측정 + val techStartTime = System.currentTimeMillis() val financialScore = FinancialAnalyzer.calculateScore(financialStmt) val scores = technicalAnalyzer.calculateScores(financialScore) + val techDuration = System.currentTimeMillis() - techStartTime + println("⏱️ [$stockName] 기술적 지표 계산 소요: ${techDuration}ms") if (scores.avg() > 50) { result(tradingDecision, false) tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport() result(tradingDecision, false) + // 4. RAG 뉴스 검색 및 임베딩 시간 측정 + val ragStartTime = System.currentTimeMillis() val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스" val questionEmbedding = embeddingModel.embed(question).content() val searchResult = embeddingStore.search( @@ -214,23 +230,36 @@ object RagService { .build() ) tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() } + val ragDuration = System.currentTimeMillis() - ragStartTime + println("⏱️ [$stockName] RAG 뉴스 검색 소요: ${ragDuration}ms") + result(tradingDecision, false) - TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}",true) - println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") - result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true) + TradingLogStore.addAnalyzer(stockName, stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}", true) + + // 5. AI 최종 결정(LLM 호출) 시간 측정 + val aiDecisionStartTime = System.currentTimeMillis() + val finalDecision = decideTrading(stockCode, scores, financialStmt, tradingDecision) + val aiDecisionDuration = System.currentTimeMillis() - aiDecisionStartTime + println("⏱️ [$stockName] AI 최종 판단 소요: ${aiDecisionDuration}ms") + + val totalDuration = System.currentTimeMillis() - totalStartTime + println("✅ [$stockName] 전체 분석 완료 총 소요: ${totalDuration}ms") + + // 상세 로그 남기기 + TradingLogStore.addAnalyzer(stockName, stockCode, "분석시간 상세: 재무(${financialDuration}ms), 뉴스(${newsIngestDuration}ms), RAG(${ragDuration}ms), AI(${aiDecisionDuration}ms)", true) + + result(finalDecision, true) } else { - println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") - TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}") + println("✋ [$stockName] 기술 점수 미달로 분석 중단") tradingDecision.confidence = 1.0 result(tradingDecision, false) } } else { - TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}") - println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}") + println("🚨 [$stockName] 재무 안전벨트 미달") tradingDecision.confidence = 1.0 result(tradingDecision, false) } - }catch (e: Exception) { + } catch (e: Exception) { e.printStackTrace() } } diff --git a/src/main/kotlin/service/DynamicNewsScraper.kt b/src/main/kotlin/service/DynamicNewsScraper.kt index e4737d8..a35266f 100644 --- a/src/main/kotlin/service/DynamicNewsScraper.kt +++ b/src/main/kotlin/service/DynamicNewsScraper.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.withTimeout import model.NewsItem import network.CorpInfo import network.RagService +import util.HardwareDetector import java.net.URL import kotlin.random.Random @@ -269,9 +270,20 @@ object DynamicNewsScraper { } object SafeScraper { + private val totalRam = HardwareDetector.getTotalRamGb() + + // RAM 8GB당 1개 수준으로 설정하되, 최대 10~12개로 제한 (CPU 부하 방지) + private val maxParallel = when { + totalRam >= 128 -> 8 + totalRam >= 64 -> 6 + totalRam >= 32 -> 4 + totalRam >= 16 -> 2 + else -> 1 + } + // 동시 처리를 1개로 줄여서 안정성을 극대화 (추천) // Playwright는 여러 페이지를 띄울 때 CPU/메모리 점유율이 매우 높습니다. - private val semaphore = Semaphore(2) + private val semaphore = Semaphore(maxParallel) suspend fun scrapeParallel(corpInfo: CorpInfo, urls: List) = coroutineScope { urls.forEach { item -> // map + awaitAll 대신 순차 처리가 현재 상황에선 더 안정적입니다. diff --git a/src/main/kotlin/service/LlamaServerManager.kt b/src/main/kotlin/service/LlamaServerManager.kt index 89f88eb..5547c5f 100644 --- a/src/main/kotlin/service/LlamaServerManager.kt +++ b/src/main/kotlin/service/LlamaServerManager.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import network.RagService +import util.HardwareDetector import java.io.BufferedReader import java.io.File import java.io.InputStreamReader @@ -46,31 +47,48 @@ object LlamaServerManager { } } - fun startServer(binPath: String, modelPath: String, port: Int, nGpuLayers: Int = 99) { - // 이미 해당 포트에서 실행 중이거나 모델 경로가 비었으면 무시합니다. + fun startServer(binPath: String, modelPath: String, port: Int) { + if (processes.containsKey(port) || modelPath.isBlank()) return val os = System.getProperty("os.name").lowercase() val arch = System.getProperty("os.arch").lowercase() val isWin = os.contains("win") - val (nGpuLayers, threads) = when { - os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64")) -> 99 to 8 - isWin -> 4 to 12 // NUC Core Ultra 7: GPU 레이어 40 내외, 스레드 12 권장 - else -> 0 to 4 // 인텔 맥 2017 등 - } + val isMacArm = os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64")) + + val cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일 + val hasGpu = HardwareDetector.hasNvidiaGpu() + +// 1. optimalThreads: 할당 비율 적용 및 최소/최대 범위 제한(Safety Boundary) +// 과도한 스레드 할당은 오히려 컨텍스트 스위칭 비용을 높여 성능을 저하시킬 수 있습니다. + val ratio = if (isWin) 0.5 else 0.7 + val optimalThreads = (cpuCores * ratio).toInt().coerceIn(4, 16) + +// 2. optimalGpuLayers: GPU 가속 조건 (윈도우 NVIDIA 또는 맥 ARM) + val optimalGpuLayers = if ((isWin && hasGpu) || isMacArm) 99 else 4 + + println("🖥️ OS: $os / Arch: $arch") + println("⚙️ 할당 스레드: $optimalThreads (Core: $cpuCores, Ratio: $ratio)") + println("🚀 GPU 레이어: $optimalGpuLayers (NVIDIA/MacArm: ${if(optimalGpuLayers == 99) "YES" else "NO"})") + +// val (nGpuLayers, threads) = when { +// os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64")) -> 99 to 8 +// isWin -> optimalGpuLayers to optimalThreads // NUC Core Ultra 7: GPU 레이어 40 내외, 스레드 12 권장 +// else -> 0 to 4 // 인텔 맥 2017 등 +// } val command = mutableListOf( binPath, "-m", modelPath, "--port", port.toString(), "-c", if (port == 8081) "512" else "8192", - "-ngl", nGpuLayers.toString(), - "-t", threads.toString(), + "-ngl", optimalGpuLayers.toString(), + "-t", optimalThreads.toString(), "--embedding" ) if (port != 8081) { // 텍스트 생성용 모델에만 적용 command.addAll(listOf( "-b", "512", // Batch size (토큰 병렬 처리량 제한으로 연산 안정화) - "--threads-batch", threads.toString(), + "--threads-batch", optimalThreads.toString(), "-fa","on" // Flash Attention 활성화 (메모리 절약 및 긴 컨텍스트 연산 안정성 증가) )) } diff --git a/src/main/kotlin/util/HardwareDetector.kt b/src/main/kotlin/util/HardwareDetector.kt new file mode 100644 index 0000000..2668433 --- /dev/null +++ b/src/main/kotlin/util/HardwareDetector.kt @@ -0,0 +1,21 @@ +package util + +object HardwareDetector { + fun getCpuCores(): Int = Runtime.getRuntime().availableProcessors() + + fun getTotalRamGb(): Long { + val bean = java.lang.management.ManagementFactory.getOperatingSystemMXBean() + as com.sun.management.OperatingSystemMXBean + return bean.totalMemorySize / (1024 * 1024 * 1024) + } + + // Windows 환경에서 NVIDIA GPU 존재 여부 확인 (간이 로직) + fun hasNvidiaGpu(): Boolean { + return try { + val process = Runtime.getRuntime().exec("nvidia-smi") + process.waitFor() == 0 + } catch (e: Exception) { + false + } + } +} \ No newline at end of file