콘텐츠로 이동

Source: report/version7/exp-expert/v7_stage05_gate_criteria.md

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_avgff_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]
- 가구별 계산: 각 household의 split array 개별 hash → 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가 다음을 출력:

  1. Gate × Run Matrix (console + markdown):
    | Gate | B0-s42 | B0-s43 | B0-s123 | B2-s42 | ... | A3-s123 |
    |------|--------|--------|---------|--------|-----|---------|
    |  1   |  PASS  |  PASS  |  PASS   |  PASS  | ... |  PASS   |
    |  2   |  n/a   |  n/a   |  n/a    |  n/a   | ... |  PASS   |
    |  3   |  PASS  |  PASS  |  PASS   |  FAIL* | ... |  PASS   |
    |  5   |  PASS  |  WARN  |  PASS   | ...              |
    
  2. Overall verdict: ALL PASS / FAIL / CRITICAL FAIL.
  3. Per-FAIL run 상세: 원인 metric 값, threshold, diff.
  4. Next action:
  5. ALL PASS → 단계 1 진입 승인.
  6. FAIL → design spec Q2 정책(자동 engineer 호출 + 사용자 알림), 재smoke 필요.
  7. 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_hash fallback).
  • Gate 5 partial pass rule의 "2개 이상 FAIL": smoke n=12 기준 ≥2/12 FAIL → overall FAIL. 향후 단계 1(25 runs) 이상으로 확장 시 비율 기준으로 전환 검토 필요.