v7 Stage 0.5 — Gate 1~6 Pass/Fail 판정 기준¶
Design spec track_v7_design.md §3의 6 Critical Gate가 smoke 12 runs
(3 cells × 2 households × 2 seeds, 또는 smoke plan 확정 후의 실제 매트릭스)에
대해 어떻게 자동 평가되는지 확정.
공통 원칙
- 데이터 소스: mlflow.search_runs() 직접 query. CSV 재사용 금지 (design §2.5).
- 공용 PAPE/HR 함수: peak_analysis.v7.metrics (definition_hash() = 8be2bd2f691deed0).
- atol: golden tensor 1e-6 (공용 모듈 고정).
- Gate 간 판정 충돌 시 §7 우선순위 적용.
- 12 runs 중 어느 하나라도 mlflow.info.status != "FINISHED"이면 Gate 평가 불가 → ERROR 상태 (FAIL 아님; 수동 개입).
Gate 1 — PAPE/HR 정의 코드 경로 동일성¶
판정 단위: all-runs-level (12 runs 전체 공통 단일 check)
근거 필드: run params pape_definition_hash, scaler_space_signature (모든 run에 필수)
자동 판정 pseudocode:
expected_hash = "8be2bd2f691deed0" # v7.metrics.definition_hash()
all_hashes = {r.data.params["pape_definition_hash"] for r in runs}
golden = v7.metrics.build_golden_tensors()
for gid in ("G1","G2","G3","G4","G5"):
expected = GOLDEN_TENSOR_EXPECTED[gid]
assert_golden_match(golden[gid], expected, atol=1e-6)
verdict = (
len(all_hashes) == 1
and all_hashes.pop() == expected_hash
and golden_tensor_check_ok
)
Pass: 12 runs 모두 pape_definition_hash = 8be2bd2f691deed0 AND G1~G5 재계산 일치 (atol=1e-6). G2는 expected NaN (degenerate policy).
Fail: 어느 run이라도 hash 불일치 OR golden tensor mismatch.
Warning: 없음 (binary gate — 허용 오차 없음. 정의 drift는 단일-소스 원칙 위반이라 중립 판정 불가).
Borderline: 없음.
Partial pass 정책: 12/12 필요 (정의 drift는 표본 크기 무관한 원칙 위반).
Gate 2 — VQ + DLinear scaler space 일치 + unit assertion¶
판정 단위: cell-level (각 cell의 VQ path와 DLinear path가 같은 normalization space에 있는가)
근거 필드:
- run params scaler_space_signature (모든 run)
- run params dlinear_scaler_space_signature, vq_scaler_space_signature (VQ cells A3에서만; A3/A4 요구)
- run params vq_input_unit, dlinear_output_unit (단위 문자열; "kW" / "kWh_per_h" / "standardized")
자동 판정 pseudocode:
for r in vq_runs: # cells with use_vq=True (A3)
s_vq = r.data.params.get("vq_scaler_space_signature")
s_dl = r.data.params.get("dlinear_scaler_space_signature")
u_vq = r.data.params.get("vq_input_unit")
u_dl = r.data.params.get("dlinear_output_unit")
if s_vq != s_dl: # signature equal → FAIL
return FAIL(f"scaler space signature mismatch in {run_id}")
if u_vq != u_dl: # unit string equal → FAIL
return FAIL(f"unit string mismatch: vq={u_vq}, dlinear={u_dl}")
# Std ratio cross-check (numerical corroboration)
std_ratio = compute_std_ratio_from_ypred(r) # std(vq_output) / std(dlinear_output)
if not (0.5 <= std_ratio <= 2.0):
return WARNING if IN [0.4, 2.5] else FAIL
"unit assertion" 해석 (결정): design spec 문구 "unit assertion"은 데이터 단위 문자열의 직접 비교로 구현.
- vq_input_unit / dlinear_output_unit 두 param의 문자열 동일성 체크
- 이 값은 engineer가 v7_runner.dispatch_cell에서 설정 (허용값: "standardized" | "kW" | "kWh_per_h" 택 1 — CELL_REGISTRY에서 공유)
- scaler signature가 동일하면 unit도 동일해야 한다는 관계는 강제되지 않는다 (다른 scaler를 써도 단위는 같을 수 있음) — 그래서 별도 check.
Pass: VQ 사용 cell (A3) 전체 3 runs에서 signature 일치 + unit 일치 + std_ratio ∈ [0.5, 2.0]. B0/B2는 VQ 미사용이라 이 gate 적용 제외. Fail: signature 불일치 OR unit 불일치 OR std_ratio ∉ [0.4, 2.5]. Warning: signature/unit 일치, std_ratio ∈ [0.4, 0.5) ∪ (2.0, 2.5]. Borderline 처리: std_ratio 경계값은 warning band (±10%). run-level log에 기록. Partial pass: A3 3/3 runs 모두 PASS 필요. 2/3 PASS는 overall FAIL (VQ drift는 cell 단위로 발현).
Gate 3 — MLflow artifact 완전성¶
판정 단위: run-level (각 run이 6항목 모두 보유)
근거 필드:
- Artifacts: checkpoints/*.pt, predictions/*y_true.npy, predictions/*y_pred.npy
- Metrics history: train_loss, val_loss (step indexed, 최소 3 points)
- Per-household metrics: pape_{hh}, hr_{hh}, mse_{hh}, mae_{hh} (4-metric × n_hh)
- Avg metrics: pape_avg, hr_avg, mse_avg, mae_avg
- Fail-fast 7-metric: ff_* prefix
자동 판정 pseudocode:
def gate3_run(run):
missing = []
arts = flatten(client.list_artifacts(run.info.run_id, recursive=True))
has_ckpt = any(a.path.startswith("checkpoints/") and a.path.endswith(".pt") for a in arts)
has_yt = any(a.path.endswith("y_true.npy") for a in arts)
has_yp = any(a.path.endswith("y_pred.npy") for a in arts)
if not has_ckpt: missing.append("checkpoint")
if not has_yt: missing.append("y_true")
if not has_yp: missing.append("y_pred")
train_hist = client.get_metric_history(run.info.run_id, "train_loss")
val_hist = client.get_metric_history(run.info.run_id, "val_loss")
if len(train_hist) < 3: missing.append("train_loss_history<3")
if len(val_hist) < 3: missing.append("val_loss_history<3")
hh_list = run.data.params["households"].split(",")
for hh in hh_list:
for m in ("pape","hr","mse","mae"):
if f"{m}_{hh}" not in run.data.metrics:
missing.append(f"{m}_{hh}")
for m in ("pape_avg","hr_avg","mse_avg","mae_avg"):
if m not in run.data.metrics: missing.append(m)
required_ff = (
"ff_final_train_loss","ff_nan_step_count","ff_val_moving_avg",
"ff_val_initial_avg","ff_val_divergence_ratio",
"ff_n_nan_predictions","ff_codebook_util",
)
for m in required_ff:
if m not in run.data.metrics: missing.append(m)
return missing
Pass: run의 missing 리스트가 빈 리스트.
Fail: run의 missing이 비어있지 않음.
Warning: 없음 (binary — 완전성 gate).
Partial pass: 12/12 PASS 필요. 1 run이라도 missing 있으면 Gate 전체 FAIL.
Gate 4 — Figure가 mlflow.search_runs() 직접 query + PNG EXIF run_id 임베드¶
판정 단위: figure-level (smoke 분석 스크립트가 생성한 figure 파일)
근거: outputs/v7_stage05/figures/smoke_*.png 각 파일의 PNG metadata
"EXIF 임베드" 해석 (결정): PNG은 전통적 EXIF를 쓰지 않는다. Pillow의 PngImagePlugin.PngInfo를 통해 PNG tEXt/iTXt 청크에 key-value 삽입한다. 실제로는 "EXIF"가 아닌 "PNG metadata chunk"지만 design spec의 관례 용어를 따라 "EXIF"로 표기. 검증은 PIL.Image.open(path).info dict 조회.
적용 범위 (결정): smoke 단계 모든 figure (최소 PAPE/HR/MSE 3종). Gate 4는 "smoke stage 산출 figure 전체"에 대한 요구이며, 향후 단계 1~6에서 생성될 모든 발표자료용 figure도 동일 규약 적용 (design §6).
자동 판정 pseudocode:
def gate4_figure(path):
img = Image.open(path)
info = img.info # PngInfo text chunks land here
required_keys = ("run_ids", "definition_hash", "created_utc", "source_experiment")
missing = [k for k in required_keys if k not in info]
if missing: return FAIL(missing)
# Must contain ALL source run_ids (list / csv)
embedded_ids = set(info["run_ids"].split(","))
actual_ids = set(run.info.run_id for run in source_runs)
if embedded_ids != actual_ids: return FAIL(f"id mismatch")
if info["definition_hash"] != "8be2bd2f691deed0": return FAIL("hash mismatch")
return PASS
필수 메타데이터 key 스킴 (확정):
| Key | Value 형식 |
|---|---|
| run_ids | 콤마 구분 32hex run_id (예: 4659…,08716…) |
| definition_hash | 8be2bd2f691deed0 (16 hex) |
| created_utc | ISO 8601 UTC timestamp (예: 2026-04-19T14:23:05Z) |
| source_experiment | MLflow experiment name (예: v7-peak-aware-fl) |
Pass: smoke 단계에서 생성된 모든 figure 파일이 4 key 모두 보유 + run_ids가 실제 분석 대상 run set과 정확히 일치.
Fail: 어느 figure라도 key missing OR run_ids mismatch OR definition_hash 불일치.
Warning: created_utc가 타임존 없는 naive string인 경우 (예: 2026-04-19T14:23:05).
Partial pass: figure 100% 준수 필요. 3종 figure 중 1종 missing이면 FAIL.
Gate 5 — 수렴성 (epoch 5+ moving avg < initial avg × 0.5)¶
판정 단위: run-level
근거 필드: metric history val_loss step=0..N-1 (총 epoch 수)
"initial" 해석 (결정): design spec "initial avg" = epoch 1~4 평균 (0-indexed: steps 0,1,2,3). v7_runner.compute_fail_fast_metrics가 이미 동일 정의(val_losses[:4])로 ff_val_initial_avg와 ff_val_moving_avg를 기록하므로, Gate 5는 이 두 metric을 직접 읽는다.
경계값 < 0.5 해석 (결정): 엄격한 strict less than (<, not ≤). 정확히 ratio = 0.5인 경우 FAIL. 이유: design spec의 수렴 기준은 "뚜렷한 감소"이고, 경계값 정확 일치는 수치 우연 가능성이 높아 보수 판정.
자동 판정 pseudocode:
def gate5_run(run):
initial_avg = run.data.metrics.get("ff_val_initial_avg")
moving_avg = run.data.metrics.get("ff_val_moving_avg")
if initial_avg is None or moving_avg is None:
return ERROR("missing ff_val_{initial,moving}_avg")
if initial_avg <= 0: # degenerate
return ERROR(f"initial_avg={initial_avg}")
ratio = moving_avg / initial_avg
if ratio < 0.5: return PASS
if ratio < 0.6: return WARNING # borderline band
return FAIL(f"ratio={ratio:.3f}")
Pass: ratio < 0.5. Warning: 0.5 ≤ ratio < 0.6 (borderline — 논문 제출 전 수동 검토 권고). Fail: ratio ≥ 0.6.
Partial pass 정책: 11-out-of-12 허용. 이유:
- smoke는 3 epoch 기본값 (_dummy_epoch_loop 기본값; 실제 smoke에서 engineer가 최소 ≥5 epoch로 설정 예정). 그래도 cell×seed 조합 중 하나가 local optimum에서 slow-start하는 경우가 있음.
- 12/12 요구는 너무 boolean하고 실제 학습 variability를 수용 못 함.
- 단, FAIL run이 2개 이상이면 overall FAIL (단일 unlucky seed와 systemic divergence 구분).
- WARNING은 PASS로 count (과도 제약 방지); 단, WARNING count ≥ 4면 overall을 WARNING으로 하강.
Gate 6 — Paired 구조 sanity (동일 seed의 모든 cell이 동일 train/val/test split data hash)¶
판정 단위: seed-level 집계 (seed별로 cell 간 hash 일치 체크)
근거 필드: run params train_data_hash, val_data_hash, test_data_hash (engineer 추가 예정)
"data hash" 정의 (결정): 각 split의 (values.tobytes() + shape + dtype) sha256 상위 16 hex.
def _split_hash(arr: np.ndarray) -> str:
h = hashlib.sha256()
h.update(arr.tobytes())
h.update(str(arr.shape).encode())
h.update(str(arr.dtype).encode())
return h.hexdigest()[:16]
train_data_hash_Apt6, train_data_hash_Apt88, … (household 수만큼 param).
- 또는 모든 household concat 후 단일 hash를 저장해도 됨 — engineer 선택. 단일 hash 방식이면 param train_data_hash (suffix 없음)로 저장.
자동 판정 pseudocode:
def gate6(runs):
# Group by seed; for each seed, all cells must share the same split hashes.
failures = []
by_seed = defaultdict(list)
for r in runs:
by_seed[int(r.data.params["seed"])].append(r)
for seed, seed_runs in by_seed.items():
for split in ("train", "val", "test"):
hashes = {r.data.params.get(f"{split}_data_hash") for r in seed_runs}
if None in hashes or len(hashes) != 1:
failures.append((seed, split, hashes))
return failures
Pass: 모든 seed에 대해 train/val/test 각각의 hash가 cell 간 동일 (3 hash × n_seed 체크).
Fail: 어느 seed-split 조합에서라도 hash set 크기 > 1.
Warning: *_data_hash param이 전부 None (engineer 미구현 상태)인 경우 → overall ERROR (Gate skip; engineer에게 issue raise).
Partial pass: 모든 seed × split 조합 100% 일치 필요 (paired 통계의 전제).
Gate × Run Matrix — 최종 판정 집계¶
12 runs × 6 gates 매트릭스에서 overall 단계 0.5 verdict 계산:
GATE 1: all-runs PASS ? → 실패 시 overall = CRITICAL FAIL (drift — 단계 1 진입 불가)
GATE 2: A3 runs PASS ? → 실패 시 overall = FAIL
GATE 3: each run missing = []? → 실패 시 overall = FAIL
GATE 4: each figure metadata OK? → 실패 시 overall = FAIL
GATE 5: partial pass rule (11/12 PASS, ≤1 FAIL) → 위반 시 overall = FAIL
GATE 6: no seed-split hash collision? → 실패 시 overall = FAIL
overall_verdict = "ALL PASS" only if every gate returns PASS (WARNING 포함 allowed).
7. Gate 간 판정 충돌 해소 (우선순위)¶
충돌 예시: run X가 Gate 3 PASS지만 Gate 5 FAIL. 최종은 무엇?
규칙 (precedence, 높은 것이 우선):
1. Gate 1 (정의 drift) — 최우선. Drift이면 이후 모든 gate 무의미 (같은 metric 계산 아님).
2. Gate 3 (artifact 완전성) — artifact 없으면 Gate 4/5/6 평가 불가. Gate 3 FAIL → 그 run은 다른 gate에서 SKIPPED 처리.
3. Gate 6 (split hash) — paired 구조 무너지면 통계적 결론 전부 invalid.
4. Gate 2 (scaler) → Gate 5 (수렴) → Gate 4 (figure) — 동등 우선순위. 각각 FAIL이면 overall FAIL에 동시 기여.
결론: "run이 Gate 3 PASS, Gate 5 FAIL"이면 → run 자체는 Gate 5 FAIL로 기록, 단계 0.5 overall verdict 계산 시 Gate 5 partial-pass rule 적용.
8. 자동 평가 산출¶
experiments/federated/v7_stage05_smoke_analysis.py가 다음을 출력:
- Gate × Run Matrix (console + markdown):
- Overall verdict:
ALL PASS/FAIL/CRITICAL FAIL. - Per-FAIL run 상세: 원인 metric 값, threshold, diff.
- Next action:
- ALL PASS → 단계 1 진입 승인.
- FAIL → design spec Q2 정책(자동 engineer 호출 + 사용자 알림), 재smoke 필요.
- CRITICAL FAIL → 단계 0 재실행 (정의 drift 수정).
9. 미결정 & 가정¶
- Gate 2 unit string 허용값: 현재
{"standardized", "kW", "kWh_per_h"}3종으로 제안. engineer가 CELL_REGISTRY에서 택일 후 param 주입하면 확정. - Gate 6 split hash 저장 방식: 가구별 suffix vs concat 단일. engineer 선택 권한. smoke 분석 스크립트는 둘 다 지원 (우선 suffix 방식 탐색, 실패 시
train_data_hashfallback). - Gate 5 partial pass rule의 "2개 이상 FAIL": smoke n=12 기준
≥2/12 FAIL → overall FAIL. 향후 단계 1(25 runs) 이상으로 확장 시 비율 기준으로 전환 검토 필요.