daniel 작업본 추가
This commit is contained in:
@@ -2,6 +2,7 @@ package com.kamco.cd.training.train;
|
|||||||
|
|
||||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||||
import com.kamco.cd.training.train.dto.TrainingMetricsDto;
|
import com.kamco.cd.training.train.dto.TrainingMetricsDto;
|
||||||
|
import com.kamco.cd.training.train.dto.TrainingProgressDto;
|
||||||
import com.kamco.cd.training.train.service.DataSetCountersService;
|
import com.kamco.cd.training.train.service.DataSetCountersService;
|
||||||
import com.kamco.cd.training.train.service.TestJobService;
|
import com.kamco.cd.training.train.service.TestJobService;
|
||||||
import com.kamco.cd.training.train.service.TrainJobService;
|
import com.kamco.cd.training.train.service.TrainJobService;
|
||||||
@@ -298,4 +299,26 @@ public class TrainApiController {
|
|||||||
trainingMetricsService.getTrainingMetricsByModelUuid(modelUuid);
|
trainingMetricsService.getTrainingMetricsByModelUuid(modelUuid);
|
||||||
return ApiResponseDto.ok(response);
|
return ApiResponseDto.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "학습 진행률 조회",
|
||||||
|
description = "UUID로 학습 진행률을 실시간 조회합니다. 기존 DB 구조를 활용하여 진행률을 계산합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "조회 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = TrainingProgressDto.class))),
|
||||||
|
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@GetMapping("/progress/{uuid}")
|
||||||
|
public ApiResponseDto<TrainingProgressDto> getTrainingProgress(
|
||||||
|
@Parameter(description = "모델 UUID", required = true) @PathVariable UUID uuid) {
|
||||||
|
TrainingProgressDto progress = trainJobService.getTrainingProgress(uuid);
|
||||||
|
return ApiResponseDto.ok(progress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.kamco.cd.training.train.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** 학습 진행률 조회 응답 DTO */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class TrainingProgressDto {
|
||||||
|
|
||||||
|
// 기본 정보
|
||||||
|
private Long jobId;
|
||||||
|
private UUID modelUuid;
|
||||||
|
private String statusCd; // QUEUED/RUNNING/SUCCESS/FAILED
|
||||||
|
private String currentPhase; // 현재 단계 (계산된 값)
|
||||||
|
private Double progressPercent; // 진행률 (0.00 ~ 100.00, 계산된 값)
|
||||||
|
|
||||||
|
// Epoch 정보 (기존 DB 컬럼)
|
||||||
|
private Integer currentEpoch; // 현재 Epoch
|
||||||
|
private Integer totalEpoch; // 전체 Epoch
|
||||||
|
|
||||||
|
// 시간 정보 (기존 DB 컬럼) - ISO 8601 형식으로 직렬화
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||||
|
private ZonedDateTime queuedDttm; // 큐 등록 시각
|
||||||
|
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||||
|
private ZonedDateTime startedDttm; // 시작 시각
|
||||||
|
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||||
|
private ZonedDateTime finishedDttm; // 종료 시각 (완료/실패 시)
|
||||||
|
|
||||||
|
// 계산된 시간 정보
|
||||||
|
private Long elapsedSeconds; // 경과 시간 (초)
|
||||||
|
private Long estimatedRemainingSeconds; // 예상 남은 시간 (초)
|
||||||
|
|
||||||
|
// 상태 메시지
|
||||||
|
private String message; // 현재 상태 메시지 (계산된 값)
|
||||||
|
|
||||||
|
// 에러 정보 (실패 시)
|
||||||
|
private String errorMessage; // 에러 메시지 (기존 DB 컬럼)
|
||||||
|
}
|
||||||
@@ -158,12 +158,42 @@ public class DockerTrainService {
|
|||||||
lastEpoch.set(epoch);
|
lastEpoch.set(epoch);
|
||||||
lastIter.set(iter);
|
lastIter.set(iter);
|
||||||
|
|
||||||
|
// Step 구분 (컨테이너 이름으로 판별)
|
||||||
|
boolean isStep1 = containerName.startsWith("train-");
|
||||||
|
boolean isStep2 = containerName.startsWith("eval-");
|
||||||
|
|
||||||
|
// 진행률 계산 (Step별 구분)
|
||||||
|
double progress = 0.0;
|
||||||
|
if (maxEpochs > 0) {
|
||||||
|
// Epoch + Iteration 기반 정밀 진행률 계산
|
||||||
|
double epochProgress =
|
||||||
|
((double) (epoch - 1) + ((double) iter / totalIter)) / maxEpochs;
|
||||||
|
|
||||||
|
// Step1 (학습): 7.5% ~ 42.5% (0% ~ 50% 중 15% ~ 85%)
|
||||||
|
// Step2 (테스트): 57.5% ~ 92.5% (50% ~ 100% 중 15% ~ 85%)
|
||||||
|
if (isStep1) {
|
||||||
|
// Step1: 0% ~ 50% 범위, 그 중 15% ~ 85% = 7.5% ~ 42.5%
|
||||||
|
progress = 7.5 + (epochProgress * 35.0);
|
||||||
|
} else if (isStep2) {
|
||||||
|
// Step2: 50% ~ 100% 범위, 그 중 15% ~ 85% = 57.5% ~ 92.5%
|
||||||
|
progress = 57.5 + (epochProgress * 35.0);
|
||||||
|
} else {
|
||||||
|
// 기본값 (하위 호환)
|
||||||
|
progress = 15.0 + (epochProgress * 70.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String stepLabel = isStep1 ? "STEP1" : isStep2 ? "STEP2" : "UNKNOWN";
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"[TRAIN] container={} epoch={} iter={}/{}",
|
"[TRAIN] {} container={} epoch={}/{} iter={}/{} | Progress: {}%",
|
||||||
|
stepLabel,
|
||||||
containerName,
|
containerName,
|
||||||
epoch,
|
epoch,
|
||||||
|
maxEpochs,
|
||||||
iter,
|
iter,
|
||||||
totalIter);
|
totalIter,
|
||||||
|
String.format("%.2f", progress));
|
||||||
|
|
||||||
modelTrainJobCoreService.updateEpoch(containerName, epoch);
|
modelTrainJobCoreService.updateEpoch(containerName, epoch);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ public class ModelTestMetricsJobService {
|
|||||||
zipFiles(zipFileList, individualZipPath);
|
zipFiles(zipFileList, individualZipPath);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"✅ 개별 ZIP 생성 완료: fileName={}, pthFile={}, size={} bytes",
|
"개별 ZIP 생성 완료: fileName={}, pthFile={}, size={} bytes",
|
||||||
individualZipName,
|
individualZipName,
|
||||||
pthFileName,
|
pthFileName,
|
||||||
Files.size(individualZipPath));
|
Files.size(individualZipPath));
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import com.kamco.cd.training.train.dto.ModelTrainJobQueuedEvent;
|
|||||||
import com.kamco.cd.training.train.dto.ModelTrainLinkDto;
|
import com.kamco.cd.training.train.dto.ModelTrainLinkDto;
|
||||||
import com.kamco.cd.training.train.dto.OutputResult;
|
import com.kamco.cd.training.train.dto.OutputResult;
|
||||||
import com.kamco.cd.training.train.dto.TrainRunRequest;
|
import com.kamco.cd.training.train.dto.TrainRunRequest;
|
||||||
|
import com.kamco.cd.training.train.dto.TrainingProgressDto;
|
||||||
|
import com.kamco.cd.training.train.util.TrainingProgressCalculator;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -466,4 +468,136 @@ public class TrainJobService {
|
|||||||
modelTrainMngCoreService.markStep1Stop(job.getModelId(), msg);
|
modelTrainMngCoreService.markStep1Stop(job.getModelId(), msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID로 학습 진행률 조회 기존 DB 컬럼만을 활용하여 실시간으로 진행률 계산
|
||||||
|
*
|
||||||
|
* @param uuid 모델 UUID
|
||||||
|
* @return 학습 진행률 정보
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public TrainingProgressDto getTrainingProgress(UUID uuid) {
|
||||||
|
// 1. UUID로 모델 조회
|
||||||
|
Long modelId = getModelIdByUuid(uuid);
|
||||||
|
ModelTrainMngDto.Basic model = modelTrainMngCoreService.findModelById(modelId);
|
||||||
|
|
||||||
|
// 2. Step 상태 확인 (step1Status, step2Status)
|
||||||
|
String step1Status = model.getStep1Status(); // READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR
|
||||||
|
String step2Status = model.getStep2Status(); // READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR
|
||||||
|
|
||||||
|
// 3. 현재 실행중인 Job ID 확인
|
||||||
|
Long jobId = model.getCurrentAttemptId();
|
||||||
|
|
||||||
|
if (jobId == null) {
|
||||||
|
// Job이 없는 경우 모델 상태만 반환
|
||||||
|
// Step1 완료 여부에 따라 진행률 결정
|
||||||
|
double progressPercent = 0.0;
|
||||||
|
String currentPhase = "NOT_STARTED";
|
||||||
|
|
||||||
|
if ("COMPLETED".equals(step1Status) && "COMPLETED".equals(step2Status)) {
|
||||||
|
progressPercent = 100.0;
|
||||||
|
currentPhase = "COMPLETED";
|
||||||
|
} else if ("COMPLETED".equals(step1Status)) {
|
||||||
|
progressPercent = 50.0;
|
||||||
|
currentPhase = "STEP1_COMPLETED";
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrainingProgressDto.builder()
|
||||||
|
.modelUuid(uuid)
|
||||||
|
.statusCd(model.getStatusCd())
|
||||||
|
.currentPhase(currentPhase)
|
||||||
|
.progressPercent(progressPercent)
|
||||||
|
.message(getMessageForPhase(currentPhase, null, null))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Job 정보 조회 (기존 컬럼만 사용)
|
||||||
|
ModelTrainJobDto job =
|
||||||
|
modelTrainJobCoreService
|
||||||
|
.findById(jobId)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Job을 찾을 수 없습니다: " + jobId));
|
||||||
|
|
||||||
|
// 5. totalEpoch 추출 (DB 컬럼 우선, 없으면 params_json에서 추출)
|
||||||
|
Integer totalEpoch = job.getTotalEpoch();
|
||||||
|
if (totalEpoch == null && job.getParamsJson() != null) {
|
||||||
|
Object totalEpochObj = job.getParamsJson().get("totalEpoch");
|
||||||
|
if (totalEpochObj != null) {
|
||||||
|
totalEpoch = Integer.valueOf(String.valueOf(totalEpochObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. jobType 추출 (TRAIN/EVAL)
|
||||||
|
String jobType = "TRAIN"; // 기본값
|
||||||
|
if (job.getParamsJson() != null) {
|
||||||
|
Object jobTypeObj = job.getParamsJson().get("jobType");
|
||||||
|
if (jobTypeObj != null) {
|
||||||
|
jobType = String.valueOf(jobTypeObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 진행률 계산 (Step 구분하여 계산)
|
||||||
|
double progressPercent =
|
||||||
|
TrainingProgressCalculator.calculateProgress(
|
||||||
|
job.getStatusCd(),
|
||||||
|
job.getCurrentEpoch(),
|
||||||
|
totalEpoch,
|
||||||
|
job.getStartedDttm(),
|
||||||
|
job.getFinishedDttm(),
|
||||||
|
jobType,
|
||||||
|
step1Status,
|
||||||
|
step2Status);
|
||||||
|
|
||||||
|
// 8. 현재 Phase 추정
|
||||||
|
String currentPhase =
|
||||||
|
TrainingProgressCalculator.estimateCurrentPhase(
|
||||||
|
job.getStatusCd(), job.getCurrentEpoch(), totalEpoch, job.getStartedDttm(), jobType);
|
||||||
|
|
||||||
|
// 9. 경과 시간 계산
|
||||||
|
Long elapsedSeconds = TrainingProgressCalculator.calculateElapsedSeconds(job.getStartedDttm());
|
||||||
|
|
||||||
|
// 10. 예상 남은 시간 계산
|
||||||
|
Long estimatedRemaining =
|
||||||
|
TrainingProgressCalculator.estimateRemainingSeconds(progressPercent, elapsedSeconds);
|
||||||
|
|
||||||
|
// 11. 상태 메시지 생성
|
||||||
|
String message =
|
||||||
|
TrainingProgressCalculator.generateProgressMessage(
|
||||||
|
currentPhase, job.getCurrentEpoch(), totalEpoch);
|
||||||
|
|
||||||
|
// 12. DTO 생성 및 반환
|
||||||
|
return TrainingProgressDto.builder()
|
||||||
|
.jobId(job.getId())
|
||||||
|
.modelUuid(uuid)
|
||||||
|
.statusCd(job.getStatusCd())
|
||||||
|
.currentPhase(currentPhase)
|
||||||
|
.progressPercent(progressPercent)
|
||||||
|
.currentEpoch(job.getCurrentEpoch())
|
||||||
|
.totalEpoch(totalEpoch)
|
||||||
|
.queuedDttm(job.getQueuedDttm())
|
||||||
|
.startedDttm(job.getStartedDttm())
|
||||||
|
.finishedDttm(job.getFinishedDttm())
|
||||||
|
.elapsedSeconds(elapsedSeconds)
|
||||||
|
.estimatedRemainingSeconds(estimatedRemaining)
|
||||||
|
.message(message)
|
||||||
|
.errorMessage(job.getErrorMessage())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase에 맞는 메시지 반환 (Job이 없는 경우)
|
||||||
|
*
|
||||||
|
* @param phase 현재 Phase
|
||||||
|
* @param currentEpoch 현재 Epoch
|
||||||
|
* @param totalEpoch 전체 Epoch
|
||||||
|
* @return 상태 메시지
|
||||||
|
*/
|
||||||
|
private String getMessageForPhase(String phase, Integer currentEpoch, Integer totalEpoch) {
|
||||||
|
if ("COMPLETED".equals(phase)) {
|
||||||
|
return "모든 학습 및 테스트가 완료되었습니다.";
|
||||||
|
} else if ("STEP1_COMPLETED".equals(phase)) {
|
||||||
|
return "학습이 완료되었습니다. 테스트를 시작할 수 있습니다.";
|
||||||
|
} else {
|
||||||
|
return "학습 작업이 시작되지 않았습니다.";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public class TrainJobWorker {
|
|||||||
(isEval ? "eval-" : "train-") + jobId + "-" + params.get("uuid").toString().substring(0, 8);
|
(isEval ? "eval-" : "train-") + jobId + "-" + params.get("uuid").toString().substring(0, 8);
|
||||||
|
|
||||||
String type = isEval ? "TEST" : "TRAIN";
|
String type = isEval ? "TEST" : "TRAIN";
|
||||||
|
String step = isEval ? "STEP2" : "STEP1";
|
||||||
|
|
||||||
Integer totalEpoch = null;
|
Integer totalEpoch = null;
|
||||||
if (params.containsKey("totalEpoch")) {
|
if (params.containsKey("totalEpoch")) {
|
||||||
@@ -65,6 +66,21 @@ public class TrainJobWorker {
|
|||||||
totalEpoch = Integer.parseInt(params.get("totalEpoch").toString());
|
totalEpoch = Integer.parseInt(params.get("totalEpoch").toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 1: 준비 단계
|
||||||
|
// Step1: 0% ~ 2.5%, Step2: 50% ~ 52.5%
|
||||||
|
double preparingProgress = isEval ? 50.0 : 0.0;
|
||||||
|
log.info(
|
||||||
|
"[JOB] {} jobId={} | Phase: PREPARING | Progress: {}%",
|
||||||
|
step, jobId, String.format("%.1f", preparingProgress + 2.5));
|
||||||
|
|
||||||
|
// Phase 2: 컨테이너 시작
|
||||||
|
// Step1: 2.5% ~ 5%, Step2: 52.5% ~ 55%
|
||||||
|
double containerStartProgress = isEval ? 52.5 : 2.5;
|
||||||
|
log.info(
|
||||||
|
"[JOB] {} jobId={} | Phase: CONTAINER_STARTING | Progress: {}%",
|
||||||
|
step, jobId, String.format("%.1f", containerStartProgress + 2.5));
|
||||||
|
|
||||||
log.info("[JOB] markRunning start jobId={}, containerName={}", jobId, containerName);
|
log.info("[JOB] markRunning start jobId={}, containerName={}", jobId, containerName);
|
||||||
// 실행 시작 처리
|
// 실행 시작 처리
|
||||||
modelTrainJobCoreService.markRunning(
|
modelTrainJobCoreService.markRunning(
|
||||||
@@ -90,12 +106,21 @@ public class TrainJobWorker {
|
|||||||
evalReq.setReqTmpYn((Boolean) params.get("reqTmpYn"));
|
evalReq.setReqTmpYn((Boolean) params.get("reqTmpYn"));
|
||||||
log.info("[JOB] selected test epoch={}", epoch);
|
log.info("[JOB] selected test epoch={}", epoch);
|
||||||
|
|
||||||
|
// Phase 3: 테스트 시작
|
||||||
|
// Step2: 55% ~ 57.5%
|
||||||
|
log.info("[JOB] STEP2 jobId={} | Phase: EVAL_STARTED | Progress: 57.5%", jobId);
|
||||||
|
|
||||||
// 도커 실행 후 로그 수집
|
// 도커 실행 후 로그 수집
|
||||||
result = dockerTrainService.runEvalSync(containerName, evalReq);
|
result = dockerTrainService.runEvalSync(containerName, evalReq);
|
||||||
} else {
|
} else {
|
||||||
// step1 진행중 처리
|
// step1 진행중 처리
|
||||||
modelTrainMngCoreService.markStep1InProgress(modelId, jobId);
|
modelTrainMngCoreService.markStep1InProgress(modelId, jobId);
|
||||||
TrainRunRequest trainReq = toTrainRunRequest(params);
|
TrainRunRequest trainReq = toTrainRunRequest(params);
|
||||||
|
|
||||||
|
// Phase 3: 학습 시작
|
||||||
|
// Step1: 5% ~ 7.5%
|
||||||
|
log.info("[JOB] STEP1 jobId={} | Phase: TRAINING_STARTED | Progress: 7.5%", jobId);
|
||||||
|
|
||||||
// 도커 실행 후 로그 수집
|
// 도커 실행 후 로그 수집
|
||||||
result = dockerTrainService.runTrainSync(trainReq, containerName);
|
result = dockerTrainService.runTrainSync(trainReq, containerName);
|
||||||
}
|
}
|
||||||
@@ -109,11 +134,25 @@ public class TrainJobWorker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 4: 학습/테스트 완료
|
||||||
|
// Step1: 42.5%, Step2: 92.5%
|
||||||
|
double completedProgress = isEval ? 92.5 : 42.5;
|
||||||
|
log.info(
|
||||||
|
"[JOB] {} jobId={} | Phase: TRAINING_COMPLETED | Progress: {}%",
|
||||||
|
step, jobId, String.format("%.1f", completedProgress));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0 정상 종료 SUCCESS 1~125 학습 코드 에러 FAILED 137 OOMKill FAILED 143 SIGTERM (stop) STOP -1 우리 내부
|
* 0 정상 종료 SUCCESS 1~125 학습 코드 에러 FAILED 137 OOMKill FAILED 143 SIGTERM (stop) STOP -1 우리 내부
|
||||||
* 강제 중단 STOP
|
* 강제 중단 STOP
|
||||||
*/
|
*/
|
||||||
if (result.getExitCode() == 0) {
|
if (result.getExitCode() == 0) {
|
||||||
|
// Phase 5: 결과 처리 중
|
||||||
|
// Step1: 45%, Step2: 95%
|
||||||
|
double processingProgress = isEval ? 95.0 : 45.0;
|
||||||
|
log.info(
|
||||||
|
"[JOB] {} jobId={} | Phase: PROCESSING_RESULTS | Progress: {}%",
|
||||||
|
step, jobId, String.format("%.1f", processingProgress));
|
||||||
|
|
||||||
// 성공 처리
|
// 성공 처리
|
||||||
modelTrainJobCoreService.markSuccess(jobId, result.getExitCode());
|
modelTrainJobCoreService.markSuccess(jobId, result.getExitCode());
|
||||||
|
|
||||||
@@ -128,6 +167,13 @@ public class TrainJobWorker {
|
|||||||
modelTrainMetricsJobService.findTrainValidMetricCsvFiles();
|
modelTrainMetricsJobService.findTrainValidMetricCsvFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 6: 완료
|
||||||
|
// Step1: 50%, Step2: 100%
|
||||||
|
double finalProgress = isEval ? 100.0 : 50.0;
|
||||||
|
log.info(
|
||||||
|
"[JOB] {} jobId={} | Phase: COMPLETED | Progress: {}%",
|
||||||
|
step, jobId, String.format("%.1f", finalProgress));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
String failMsg = result.getStatus() + "\n" + result.getLogs();
|
String failMsg = result.getStatus() + "\n" + result.getLogs();
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package com.kamco.cd.training.train.util;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 학습 진행률 계산 유틸리티 기존 DB 컬럼만을 활용하여 진행률을 실시간 계산
|
||||||
|
*
|
||||||
|
* <p>상태전의 STATUS 가이드 기준: - Step1 (학습): TRAIN jobType → 0% ~ 50% - Step2 (테스트): EVAL jobType → 50% ~
|
||||||
|
* 100%
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public class TrainingProgressCalculator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 학습 진행률 계산 (Step 구분)
|
||||||
|
*
|
||||||
|
* @param statusCd 작업 상태 (QUEUED/RUNNING/SUCCESS/FAILED/STOPPED/CANCELED)
|
||||||
|
* @param currentEpoch 현재 Epoch
|
||||||
|
* @param totalEpoch 전체 Epoch
|
||||||
|
* @param startedDttm 시작 시각
|
||||||
|
* @param finishedDttm 종료 시각
|
||||||
|
* @param jobType 작업 타입 (TRAIN/EVAL)
|
||||||
|
* @param step1State Step1 상태 (READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR)
|
||||||
|
* @param step2State Step2 상태 (READY/IN_PROGRESS/COMPLETED/STOPPED/ERROR)
|
||||||
|
* @return 진행률 (0.00 ~ 100.00)
|
||||||
|
*/
|
||||||
|
public static double calculateProgress(
|
||||||
|
String statusCd,
|
||||||
|
Integer currentEpoch,
|
||||||
|
Integer totalEpoch,
|
||||||
|
ZonedDateTime startedDttm,
|
||||||
|
ZonedDateTime finishedDttm,
|
||||||
|
String jobType,
|
||||||
|
String step1State,
|
||||||
|
String step2State) {
|
||||||
|
if (statusCd == null) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step별 진행률 계산
|
||||||
|
boolean isStep1 = "TRAIN".equals(jobType);
|
||||||
|
boolean isStep2 = "EVAL".equals(jobType) || "TEST".equals(jobType);
|
||||||
|
|
||||||
|
// Step1 완료, Step2 완료 여부 확인
|
||||||
|
boolean isStep1Completed = "COMPLETED".equals(step1State);
|
||||||
|
boolean isStep2Completed = "COMPLETED".equals(step2State);
|
||||||
|
|
||||||
|
switch (statusCd) {
|
||||||
|
case "QUEUED":
|
||||||
|
// Step1 대기 중: 0%, Step2 대기 중: 50%
|
||||||
|
return isStep2 ? 50.0 : 0.0;
|
||||||
|
|
||||||
|
case "RUNNING":
|
||||||
|
return calculateRunningProgress(
|
||||||
|
currentEpoch, totalEpoch, startedDttm, isStep1, isStep2, isStep1Completed);
|
||||||
|
|
||||||
|
case "SUCCESS":
|
||||||
|
// Step1 성공: 50%, Step2 성공: 100%
|
||||||
|
if (isStep2 || isStep2Completed) {
|
||||||
|
return 100.0;
|
||||||
|
} else if (isStep1 || isStep1Completed) {
|
||||||
|
return 50.0;
|
||||||
|
}
|
||||||
|
return 100.0; // 기본값
|
||||||
|
|
||||||
|
case "FAILED":
|
||||||
|
case "STOPPED":
|
||||||
|
case "CANCELED":
|
||||||
|
// 실패/취소된 경우 마지막 진행률 반환
|
||||||
|
return calculateRunningProgress(
|
||||||
|
currentEpoch, totalEpoch, startedDttm, isStep1, isStep2, isStep1Completed);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 중인 학습의 진행률 계산 (Step 구분)
|
||||||
|
*
|
||||||
|
* <p>Step1 (학습): 0% ~ 50% - PREPARING: 0% ~ 2.5% - CONTAINER_STARTING: 2.5% ~ 5% - DATA_LOADING:
|
||||||
|
* 5% ~ 7.5% - TRAINING: 7.5% ~ 42.5% (35% 비중) - PROCESSING: 42.5% ~ 50%
|
||||||
|
*
|
||||||
|
* <p>Step2 (테스트): 50% ~ 100% - PREPARING: 50% ~ 52.5% - CONTAINER_STARTING: 52.5% ~ 55% -
|
||||||
|
* DATA_LOADING: 55% ~ 57.5% - TESTING: 57.5% ~ 92.5% (35% 비중) - PROCESSING: 92.5% ~ 100%
|
||||||
|
*/
|
||||||
|
private static double calculateRunningProgress(
|
||||||
|
Integer currentEpoch,
|
||||||
|
Integer totalEpoch,
|
||||||
|
ZonedDateTime startedDttm,
|
||||||
|
boolean isStep1,
|
||||||
|
boolean isStep2,
|
||||||
|
boolean isStep1Completed) {
|
||||||
|
|
||||||
|
// Step 기준 계산
|
||||||
|
double baseProgress = 0.0;
|
||||||
|
double maxProgress = 50.0;
|
||||||
|
|
||||||
|
if (isStep2 || isStep1Completed) {
|
||||||
|
// Step2 진행 중 또는 Step1 완료 후
|
||||||
|
baseProgress = 50.0;
|
||||||
|
maxProgress = 100.0;
|
||||||
|
} else if (isStep1) {
|
||||||
|
// Step1 진행 중
|
||||||
|
baseProgress = 0.0;
|
||||||
|
maxProgress = 50.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double stepRange = maxProgress - baseProgress; // 50%
|
||||||
|
|
||||||
|
// 시작 직후 (Epoch 정보 없음)
|
||||||
|
if (currentEpoch == null || totalEpoch == null || totalEpoch == 0) {
|
||||||
|
return baseProgress + estimateInitialPhaseProgress(startedDttm, stepRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Epoch 0 또는 1 (학습 시작 단계)
|
||||||
|
if (currentEpoch <= 1) {
|
||||||
|
return baseProgress + (stepRange * 0.15); // 7.5% (Step1) 또는 57.5% (Step2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 학습 진행 중 (15% ~ 85% of step range)
|
||||||
|
double epochProgress = (double) currentEpoch / totalEpoch;
|
||||||
|
double trainingProgress = 0.15 + (epochProgress * 0.70); // 15% ~ 85%
|
||||||
|
|
||||||
|
// 학습 완료 (마지막 Epoch)
|
||||||
|
if (currentEpoch >= totalEpoch) {
|
||||||
|
// 85% ~ 100% 사이로 추정
|
||||||
|
return baseProgress + (stepRange * 0.90);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseProgress + (stepRange * trainingProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초기 단계 진행률 추정 (시작 시간 기반)
|
||||||
|
*
|
||||||
|
* @param startedDttm 시작 시각
|
||||||
|
* @param stepRange Step 범위 (50%)
|
||||||
|
* @return 초기 단계 진행률 (0 ~ 0.15 비율)
|
||||||
|
*/
|
||||||
|
private static double estimateInitialPhaseProgress(ZonedDateTime startedDttm, double stepRange) {
|
||||||
|
if (startedDttm == null) {
|
||||||
|
return stepRange * 0.05; // 5% of step range
|
||||||
|
}
|
||||||
|
|
||||||
|
long elapsedSeconds = ChronoUnit.SECONDS.between(startedDttm, ZonedDateTime.now());
|
||||||
|
|
||||||
|
// 시작 후 30초 이내: PREPARING (0% ~ 5% of step)
|
||||||
|
if (elapsedSeconds < 30) {
|
||||||
|
return Math.min(stepRange * 0.05, (elapsedSeconds / 30.0) * stepRange * 0.05);
|
||||||
|
}
|
||||||
|
// 30초 ~ 60초: CONTAINER_STARTING (5% ~ 10% of step)
|
||||||
|
else if (elapsedSeconds < 60) {
|
||||||
|
return stepRange * 0.05 + ((elapsedSeconds - 30) / 30.0) * stepRange * 0.05;
|
||||||
|
}
|
||||||
|
// 60초 이상: DATA_LOADING (10% ~ 15% of step)
|
||||||
|
else {
|
||||||
|
return Math.min(
|
||||||
|
stepRange * 0.15, stepRange * 0.10 + ((elapsedSeconds - 60) / 60.0) * stepRange * 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 Phase 추정
|
||||||
|
*
|
||||||
|
* @param statusCd 작업 상태
|
||||||
|
* @param currentEpoch 현재 Epoch
|
||||||
|
* @param totalEpoch 전체 Epoch
|
||||||
|
* @param startedDttm 시작 시각
|
||||||
|
* @param jobType 작업 타입 (TRAIN/EVAL)
|
||||||
|
* @return 현재 Phase
|
||||||
|
*/
|
||||||
|
public static String estimateCurrentPhase(
|
||||||
|
String statusCd,
|
||||||
|
Integer currentEpoch,
|
||||||
|
Integer totalEpoch,
|
||||||
|
ZonedDateTime startedDttm,
|
||||||
|
String jobType) {
|
||||||
|
if ("SUCCESS".equals(statusCd)) {
|
||||||
|
return "COMPLETED";
|
||||||
|
}
|
||||||
|
if ("FAILED".equals(statusCd)) {
|
||||||
|
return "FAILED";
|
||||||
|
}
|
||||||
|
if ("STOPPED".equals(statusCd) || "CANCELED".equals(statusCd)) {
|
||||||
|
return "CANCELED";
|
||||||
|
}
|
||||||
|
if ("QUEUED".equals(statusCd)) {
|
||||||
|
return "QUEUED";
|
||||||
|
}
|
||||||
|
|
||||||
|
// RUNNING 상태
|
||||||
|
boolean isStep2 = "EVAL".equals(jobType) || "TEST".equals(jobType);
|
||||||
|
String prefix = isStep2 ? "STEP2_" : "STEP1_";
|
||||||
|
|
||||||
|
if (currentEpoch == null || totalEpoch == null) {
|
||||||
|
if (startedDttm == null) {
|
||||||
|
return prefix + "PREPARING";
|
||||||
|
}
|
||||||
|
long elapsed = ChronoUnit.SECONDS.between(startedDttm, ZonedDateTime.now());
|
||||||
|
if (elapsed < 30) return prefix + "PREPARING";
|
||||||
|
if (elapsed < 60) return prefix + "CONTAINER_STARTING";
|
||||||
|
return prefix + "DATA_LOADING";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentEpoch >= totalEpoch) {
|
||||||
|
return prefix + "PROCESSING_RESULTS";
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + "TRAINING";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 경과 시간 계산 (초) */
|
||||||
|
public static Long calculateElapsedSeconds(ZonedDateTime startedDttm) {
|
||||||
|
if (startedDttm == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ChronoUnit.SECONDS.between(startedDttm, ZonedDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 예상 남은 시간 계산 (초) */
|
||||||
|
public static Long estimateRemainingSeconds(double progressPercent, Long elapsedSeconds) {
|
||||||
|
if (elapsedSeconds == null || progressPercent <= 0.0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double remaining = (100.0 - progressPercent) / progressPercent;
|
||||||
|
return (long) (elapsedSeconds * remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 메시지 생성
|
||||||
|
*
|
||||||
|
* @param phase 현재 Phase
|
||||||
|
* @param currentEpoch 현재 Epoch
|
||||||
|
* @param totalEpoch 전체 Epoch
|
||||||
|
* @return 상태 메시지
|
||||||
|
*/
|
||||||
|
public static String generateProgressMessage(
|
||||||
|
String phase, Integer currentEpoch, Integer totalEpoch) {
|
||||||
|
if (phase == null) {
|
||||||
|
return "대기중";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 구분
|
||||||
|
boolean isStep1 = phase.startsWith("STEP1_");
|
||||||
|
boolean isStep2 = phase.startsWith("STEP2_");
|
||||||
|
String stepName = isStep1 ? "학습" : isStep2 ? "테스트" : "";
|
||||||
|
|
||||||
|
switch (phase) {
|
||||||
|
case "QUEUED":
|
||||||
|
return "학습 작업이 큐에 등록되었습니다.";
|
||||||
|
|
||||||
|
case "STEP1_PREPARING":
|
||||||
|
return "학습 준비 중입니다.";
|
||||||
|
case "STEP1_CONTAINER_STARTING":
|
||||||
|
return "학습용 Docker 컨테이너를 시작하는 중입니다.";
|
||||||
|
case "STEP1_DATA_LOADING":
|
||||||
|
return "학습 데이터를 로딩하는 중입니다.";
|
||||||
|
case "STEP1_TRAINING":
|
||||||
|
if (currentEpoch != null && totalEpoch != null) {
|
||||||
|
return String.format("학습 진행 중 (Epoch %d/%d)", currentEpoch, totalEpoch);
|
||||||
|
}
|
||||||
|
return "학습 진행 중";
|
||||||
|
case "STEP1_PROCESSING_RESULTS":
|
||||||
|
return "학습 결과를 처리하는 중입니다.";
|
||||||
|
|
||||||
|
case "STEP2_PREPARING":
|
||||||
|
return "테스트 준비 중입니다.";
|
||||||
|
case "STEP2_CONTAINER_STARTING":
|
||||||
|
return "테스트용 Docker 컨테이너를 시작하는 중입니다.";
|
||||||
|
case "STEP2_DATA_LOADING":
|
||||||
|
return "테스트 데이터를 로딩하는 중입니다.";
|
||||||
|
case "STEP2_TRAINING":
|
||||||
|
if (currentEpoch != null && totalEpoch != null) {
|
||||||
|
return String.format("테스트 진행 중 (Epoch %d/%d)", currentEpoch, totalEpoch);
|
||||||
|
}
|
||||||
|
return "테스트 진행 중";
|
||||||
|
case "STEP2_PROCESSING_RESULTS":
|
||||||
|
return "테스트 결과를 처리하는 중입니다.";
|
||||||
|
|
||||||
|
case "COMPLETED":
|
||||||
|
return "학습이 성공적으로 완료되었습니다.";
|
||||||
|
case "FAILED":
|
||||||
|
return "학습이 실패했습니다.";
|
||||||
|
case "CANCELED":
|
||||||
|
return "학습이 취소되었습니다.";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return stepName + " 진행 중";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user