야간 정밀도 회귀 테스트
야간 정밀도 회귀 테스트 (Nightly Precision Regression Testing)
이 문서는 SGLang 서빙 엔진에서 조용한 수치 회귀(silent numerical regression)를 감지하는 야간 정밀도 회귀 프레임워크를 설명해요. 연속 실행 사이의 레이어별 hidden state를 비교해서 감지하고, 8×H200 GPU에서 야간 CI 작업으로 실행돼요. 개발과 디버깅을 위해 로컬에서도 호출할 수 있어요.
출처: 문서
본문
개요 (Overview)
야간 정밀도 회귀 프레임워크는 연속 실행 사이의 레이어별 hidden states를 비교해 SGLang 서빙 엔진의 조용한 수치 회귀를 감지해요. 8×H200 GPU에서 야간 CI 작업으로 실행되며 개발과 디버깅을 위해 로컬에서도 호출할 수 있어요.
프레임워크는 롤링 기준선(rolling-baseline) 모델로 동작해요:
- 기준선 생성 또는 비교: 서버를 실행하고 고정 프롬프트를 보내며 레이어별 hidden states를 디스크에 덤프해요. 이전 기준선이 있으면 SGLang 텐서 비교기로 새 텐서를 비교해요. 비교가 통과하면 새 텐서가 업데이트된 기준선이 돼요.
- 첫 실행(또는 캡처 형태가 바뀔 때)에서는 비교 없이 덤프된 텐서가 새 기준선으로 저장돼요.
기준선은 로컬 디스크에 저장되고 HuggingFace 데이터셋에 동기화되어 CI 러너를 넘어 생존하고 머신 간에 공유될 수 있어요. HF 데이터셋 저장소는 필수이며, SGLANG_PRECISION_HF_REPO가 설정되지 않으면 테스트가 오류를 냅니다.
동작 방식 (How It Works)
단계별 흐름 (Step-by-step flow)
┌──────────────────────────────────────────────────────────────┐
│ 1. Resolve model config (layer count, capture layers) │
│ ↓ │
│ 2. Compute capture_signature (schema, layers, TP, filter) │
│ ↓ │
│ 3. Fetch baseline from HF dataset (signature-matched) │
│ ↓ │
│ 4. Launch SGLang server with DUMPER enabled │
│ ↓ │
│ 5. POST /dumper/configure (set layer filter + cleanup) │
│ ↓ │
│ 6. POST /v1/chat/completions (fixed prompt, 2 tokens, │
│ ignore_eos=true to force decode path) │
│ ↓ │
│ 7. Kill server; assert decode tensors were captured │
│ ↓ │
│ 8. Baseline exists (with matching signature)? │
│ ├── YES → Run comparator → pass/fail │
│ │ ├── PASS → update baseline, push to HF │
│ │ └── FAIL → push diagnostics to HF │
│ └── NO → copy today's tensors as initial baseline │
│ → push to HF as "baseline_established" │
│ ↓ │
│ 9. Report summary (stdout + GitHub Step Summary) │
└──────────────────────────────────────────────────────────────┘
핵심 구성 요소 (Key components)
| Component | File | Purpose |
|---|---|---|
| 테스트 진입점 | test/registered/debug_utils/test_nightly_precision_regression.py |
서버 실행, 덤프, 비교, 보고를 조정 |
| HF 기준선 저장소 | python/sglang/test/precision_baseline_store.py |
HuggingFace 데이터셋에서 기준선 push / fetch / prune |
| 텐서 비교기 | python/sglang/srt/debug_utils/comparator/ |
두 디렉터리의 .pt 텐서를 비교, JSONL 보고서 생성 |
| Dumper 인프라 | python/sglang/srt/debug_utils/dumper.py |
런타임에 레이어별 hidden states 캡처 |
| CI 워크플로우 | .github/workflows/nightly-test-nvidia.yml |
8×H200에서 야간 작업 스케줄링 |
덤프되고 비교되는 것 (What Gets Dumped and Compared)
Strided 레이어 캡처
모든 레이어가 덤프되지는 않아요. 프레임워크는 I/O와 저장 오버헤드를 줄이기 위해 strided capture를 사용해요. 기본적으로 다음을 캡처해요:
- Layer 0 (항상)
- 마지막 레이어 (항상)
- 그 사이의 8번째 레이어마다 (
LAYER_CAPTURE_STRIDE로 구성 가능)
레이어 수는 모델의 HuggingFace config.json(num_hidden_layers 또는 num_layers)에서 자동 해석돼요. 해석이 실패하면 안전 폴백으로 모든 레이어가 캡처돼요.
Dumper 필터는 선택된 레이어 인덱스만 일치하는 정규식으로 동적으로 구성돼요. 예:
match(r'^non_intrusive__model\.layers\.(0|7|15|23)\.(inputs\.1|self_attn\.inputs\.hidden_states)$', name)
Decode 경로 검증
테스트는 ignore_eos=True로 2 토큰을 생성해 모델의 decode 경로가 실행되도록 보장해요. 덤프 후 _assert_decode_captured()가 decode 스텝의 텐서가 실제로 캡처됐는지(prefill만이 아닌) 검증해요. prefill 텐서만 있으면 --max-total-tokens이 decode 루프가 실행되기엔 너무 낮은 설정 오류를 잡기 위해 테스트가 즉시 실패해요.
비교기 (Comparator)
비교기는 각 텐서에 대해 상대 차이(rel_diff)를 계산하고 구성 가능한 임계값(기본 1e-3)에 대해 확인해요. 비교는 post-fusion 어텐션 입력으로 제한되며, --override-dims가 TP 랭크에 걸쳐 복제된다고 선언해요:
--filter self_attn\.inputs\.hidden_states
--override-dims ^non_intrusive__model\.layers\.\d+\.self_attn\.inputs\.hidden_states$:bs h # tp:replicated
축이 복제된 것으로 선언하면 비교기가 각 랭크가 부분 기여를 합산하는 대신 모든 랭크가 같은 값을 보유하는지 확인해요. 레이어 진입 inputs.1 텐서는 여전히 캡처되지만 --filter로 비교에서 제외돼요(아래 참고).
비교기가 exit code 0을 반환하지만 0 레이어를 비교한 경우(기준선/대상 이름 불일치), 테스트는 조용히 통과하는 대신 진단 메시지로 실패해요.
캡처 시그니처 (Capture signature)
capture_signature(스키마 버전, max_tokens, ignore_eos, TP 크기, dumper 필터, 비교기 필터, fusion 백엔드의 SHA-1 해시)가 실행마다 계산돼요. HF 저장소는 fetch 중 이 시그니처를 사용해 동일한 캡처 및 비교 계약을 가진 기준선만 고려되도록 해요. 시그니처가 바뀌면(예: 캡처 집합에 레이어 추가 또는 TP 변경) 호환되지 않는 텐서에서 오류를 내는 대신 새 기준선을 수립해요.
Fusion과 무엇을 비교하는지
Harness는 FlashInfer all-reduce fusion을 켜 두는데(--flashinfer-allreduce-fusion-backend trtllm; SM90 자동 활성화는 #23402에서 제거됨) 레이어당 두 개의 이름을 캡처해요:
inputs.1— 레이어로 들어가는 hidden states.LayerCommunicator는 크로스-레이어 all-reduce를 지연시키므로, fusion 활성화 시 이들은 rank-local TP-partial sum이에요. CPU 측 감소는 커널의 의미적 출력이 아니고 self-hosted 러너에 걸쳐 드리프트하므로 비교되지 않아요._assert_fused_tp_layout()는 이들을 가드로만 유지해요. 복제된 형태로 돌아오면 fusion이 조용히 폴백된 것이므로 테스트가 실패해요.self_attn.inputs.hidden_states—prepare_attn()후 어텐션이 소비하는 값, 즉 fused all-reduce와 residual RMSNorm 이후. 랭크에 걸쳐 복제되며 비교기가 실제로 diff하는 대상이에요.
이것이 fusion이 초기화되지 않은 상태로 캡처된 낡은 기준선이 호환되지 않는 이유예요. 대상이 TP-partial 텐서를 보유한 곳에 복제된 레이어 진입 텐서를 보유하게 되어 대략 tp_size배 불일치로 읽히기 때문이에요. Fusion 초기화 실패는 FlashInfer #3676과 SGLang #30875에서 추적돼요.
환경 변수 (Environment Variables)
| Variable | Default | Description |
|---|---|---|
SGLANG_PRECISION_MODELS |
zai-org/GLM-5.1-FP8 |
테스트할 쉼표로 구분된 HuggingFace 모델 ID |
SGLANG_PRECISION_BASELINE_DIR |
/tmp/sglang_precision_baselines |
기준선 텐서용 로컬 디렉터리 |
SGLANG_PRECISION_DIFF_THRESHOLD |
1e-3 |
텐서당 상대 diff 임계값 |
SGLANG_PRECISION_FORCE_UPDATE |
0 |
1로 설정하면 비교를 건너뛰고 기준선을 무조건 새로 고침 |
SGLANG_PRECISION_COMMIT |
(git에서 자동 감지) | push 시 태그되는 sglang commit SHA 오버라이드 |
SGLANG_PRECISION_HF_REPO |
(필수) | 크로스 러너 기준선 저장용 HuggingFace 데이터셋 repo |
SGLANG_PRECISION_HF_REVISION |
main |
HF 데이터셋의 브랜치/리비전 |
SGLANG_PRECISION_HF_TOKEN |
(CI에서 필수) | 데이터셋에 쓰기 권한이 있는 HuggingFace 토큰. 러너의 gated-model 읽기 토큰을 담은 HF_TOKEN과 분리해 유지 |
SGLANG_PRECISION_HF_READ_ONLY |
0 |
1로 설정하면 공유 기준선 저장소를 업데이트하지 않고 fetch 및 비교만 |
CI 통합 (CI Integration)
워크플로우 작업 (Workflow job)
테스트는 .github/workflows/nightly-test-nvidia.yml의 nightly-8-gpu-h200 스테이지에 등록돼요. 이는 다른 모든 CUDA 스테이지처럼 _pr-test-stage.yml을 통해 실행돼요. 아래 기준선 env는 모든 예약 스테이지에서 내보내지며, 이 테스트만 읽어요.
주요 CI 구성:
- name: Export precision baseline env
if: inputs.scheduled
env:
BASELINE_HF_TOKEN: ${{ secrets.HF_TOKEN_PRECISION_STORE }}
run: |
{
echo "SGLANG_PRECISION_BASELINE_DIR=/tmp/sglang_precision_baselines"
echo "SGLANG_PRECISION_HF_REPO=${{ vars.SGLANG_PRECISION_HF_REPO }}"
echo "SGLANG_PRECISION_HF_REVISION=${{ vars.SGLANG_PRECISION_HF_REVISION || 'main' }}"
echo "SGLANG_PRECISION_COMMIT=${{ github.sha }}"
echo "SGLANG_PRECISION_HF_TOKEN=${BASELINE_HF_TOKEN}"
} >> "$GITHUB_ENV"
HF_TOKEN이 아닌 SGLANG_PRECISION_HF_TOKEN을 사용해요. 후자는 이미 러너의 gated-model 읽기 토큰을 담고 있으며, 이를 덮어쓰면 작업의 모든 gated 모델이 401이 돼요.
독립형 /rerun-test 실행은 야간 기준선을 변경하지 않고 fetch해요. 유지 관리자는 in-repo PR에서 /rerun-test --refresh-precision-baseline test/registered/debug_utils/test_nightly_precision_regression.py로 새로 고칠 수 있어요.
필수 GitHub secrets/variables
| Name | Type | Purpose |
|---|---|---|
SGLANG_PRECISION_HF_REPO |
Repository variable | HF 데이터셋 repo ID(예: org/sglang-precision-baselines) — 필수, 미설정 시 테스트 오류 |
SGLANG_PRECISION_HF_REVISION |
Repository variable (optional) | 데이터셋 브랜치(기본값 main) |
HF_TOKEN_PRECISION_STORE |
Repository secret | 데이터셋에 쓰기 권한이 있는 HF 토큰. SGLANG_PRECISION_HF_TOKEN으로 작업에 내보냄 |
GitHub 스텝 요약 (GitHub Step Summary)
CI에서 실행할 때 테스트는 GitHub Actions 작업 요약에 각 모델의 상태(PASSED, FAILED, BASELINE_ESTABLISHED, ERROR)를 보여주는 Markdown 표를 씁니다.
HF 데이터셋 저장 레이아웃 (HF Dataset Storage Layout)
기준선은 HF 데이터셋에서 다음으로 구성됩니다:
<model_sanitized>/<YYYY>/<MM>/<DD>/run-<sha7>/
├── meta.json # Run metadata (model, commit, hardware, thresholds, stats)
├── comparator_report.jsonl # Per-tensor comparison results
└── tensors/
├── layer_0_inputs_1.pt
├── layer_7_inputs_1.pt
└── ...
최상위 manifest.jsonl은 모든 실행을 행당 하나의 JSON 객체로 추적해요. 각 manifest 행은 capture_signature 필드를 담아 fetch가 일치하는 캡처 형태를 가진 기준선만 선택하도록 해요.
prune_old_runs() 함수(수동 호출 가능)는 일일 실행을 30일 유지하고 그 창을 넘어서는 주간 실행을 하나씩 유지해요.
새 모델 추가 방법 (How to Add a New Model)
옵션 A: 기본 모델 목록에 추가 (CI)
test/registered/debug_utils/test_nightly_precision_regression.py의 기본값을 편집해요:
DEFAULT_MODELS_FOR_NIGHTLY_PRECISION = "zai-org/GLM-5.1-FP8,your-org/your-model"
또는 CI 워크플로우에서 SGLANG_PRECISION_MODELS 환경 변수를 설정해 기본값을 재정의해요.
옵션 B: 특정 모델로 로컬 실행
export SGLANG_PRECISION_MODELS="your-org/your-model"
export SGLANG_PRECISION_BASELINE_DIR="/tmp/my_precision_baselines"
export SGLANG_PRECISION_DIFF_THRESHOLD="1e-3"
export SGLANG_PRECISION_HF_REPO="your-org/sglang-precision-baselines"
export SGLANG_PRECISION_HF_TOKEN="hf_..."
cd test
python3 -m pytest registered/debug_utils/test_nightly_precision_regression.py -v
단계별: 야간 CI에 모델 추가
-
모델이 dumper와 동작하는지 검증. 먼저 로컬에서 실행해 hidden states가 올바르게 캡처되는지 확인:
export SGLANG_PRECISION_MODELS="your-org/your-model" export SGLANG_PRECISION_BASELINE_DIR="/tmp/test_baselines" export SGLANG_PRECISION_HF_REPO="your-org/sglang-precision-baselines" export SGLANG_PRECISION_HF_TOKEN="hf_..." export SGLANG_PRECISION_FORCE_UPDATE="1" # first run: establish baseline cd test python3 -m pytest registered/debug_utils/test_nightly_precision_regression.py -v -k test_precision -
비교 패스 실행(
FORCE_UPDATE제거):unset SGLANG_PRECISION_FORCE_UPDATE python3 -m pytest registered/debug_utils/test_nightly_precision_regression.py -v -k test_precision엔진이 그 모델에 대해 수치적으로 안정적이면
PASSED를 보고해야 해요. -
텐서 병렬 크기 설정. 모델이 TP > 1을 요구하면 테스트 harness는 모든 모델에 기본
tp_size=8을 사용해요. 커스터마이즈하려면 테스트에서ModelLaunchSettings구성을 수정하거나 추가 서버 인자를 전달해요:# In setUpClass or via env-driven logic cls.models = [ModelLaunchSettings("your-org/your-model", tp_size=4)] -
필요 시 diff 임계값 조정. FP8 또는 양자화 모델은 더 큰 수치 차이를 보일 수 있어요.
SGLANG_PRECISION_DIFF_THRESHOLD를 적절한 값(예: FP8용1e-2)으로 설정해요. -
기본 모델 목록에 추가하거나 CI 워크플로우에서
SGLANG_PRECISION_MODELS구성.
모델별 조정 고려 사항
| Concern | How to handle |
|---|---|
| TP 크기 != 8 | ModelLaunchSettings에서 tp_size 오버라이드 또는 모델별 로직 추가 |
| 양자화 모델 (FP8, GPTQ) | SGLANG_PRECISION_DIFF_THRESHOLD 완화 (예: 1e-2) |
| 모델이 추가 서버 인자 필요 | ModelLaunchSettings(model, extra_args=["--quantization", "fp8"])로 전달 |
| 모델이 다른 프롬프트 필요 | PROMPT 상수 수정 또는 모델 구성 가능하게 만들기 |
| TP partial sums를 가진 MoE 모델 | --override-dims(bs h[tp:partial])로 이미 처리됨 |
| 더 적거나 많은 캡처 레이어 | LAYER_CAPTURE_STRIDE(기본 8) 조정; 작은 모델에는 더 낮게 설정 |
| Decode가 캡처되지 않음 | --max-total-tokens이 스케줄러의 decode 예약(기본 512)보다 훨씬 높은지 확인; 테스트는 4096 사용 |
로컬 실행 (Running Locally)
전제 조건 (Prerequisites)
- 개발 모드로 설치된 SGLang
- 모델 요구 사항과 일치하는 GPU
huggingface_hub설치- 기준선 저장용 HuggingFace 데이터셋 및 쓰기 가능한
SGLANG_PRECISION_HF_TOKEN. HF 저장소는 필수 —SGLANG_PRECISION_HF_REPO가 설정되어야 하며, 그렇지 않으면 시작 시 테스트가 오류를 내요. 야간 CI 러너는 임시(영구 로컬 디스크 없음)이므로 기준선이 HF 데이터셋을 통해 실행 간에 생존해야 하기 때문이에요. 현재 로컬 전용 폴백은 없어요.
빠른 로컬 테스트
# All three are required — the test errors if SGLANG_PRECISION_HF_REPO is unset.
export SGLANG_PRECISION_MODELS="Qwen/Qwen2.5-0.5B-Instruct"
export SGLANG_PRECISION_BASELINE_DIR="/tmp/precision_baselines"
export SGLANG_PRECISION_HF_REPO="your-org/sglang-precision-baselines"
export SGLANG_PRECISION_HF_TOKEN="hf_..."
# First run: establish baseline
cd test
python3 -m pytest registered/debug_utils/test_nightly_precision_regression.py -v
# Second run: compare against baseline
python3 -m pytest registered/debug_utils/test_nightly_precision_regression.py -v
기준선 강제 새로 고침
export SGLANG_PRECISION_FORCE_UPDATE="1"
python3 -m pytest registered/debug_utils/test_nightly_precision_regression.py -v
결과 해석 (Interpreting Results)
상태 코드 (Status codes)
| Status | Meaning |
|---|---|
BASELINE_ESTABLISHED |
일치하는 시그니처의 이전 기준선이 없었음; 오늘의 텐서가 새 기준선으로 저장됨 |
PASSED |
모든 레이어별 hidden states가 diff 임계값 안에 있음; 기준선 업데이트 |
FAILED |
하나 이상의 레이어가 diff 임계값을 초과하거나, 0개 레이어가 비교됨(기준선/대상 불일치); 진단 데이터를 HF로 push |
ERROR |
서버 실행, 추론 또는 비교에서 예상치 못한 오류 발생 |
출력 예시
============================================================
Nightly Precision Regression Summary
============================================================
Model Status Details
------------------------------------------------------------
zai-org/GLM-5.1-FP8 PASSED comparison ok, baseline updated
Qwen/Qwen2.5-0.5B-Instruct FAILED tensor=layer_23.inputs_1 rel_diff=0.0152
============================================================
실패 감지 시 (When a failure is detected)
- 비교기 출력은
/tmp/nightly_precision_<model>_*.log에 저장 - 실패한 텐서와 비교기 보고서는
pass_label="failed"로 HF 데이터셋에 push되어 오프라인 진단 가능 - GitHub 스텝 요약에 실패 세부 사항 포함
- CI 작업이 0이 아닌 상태로 종료
기준선 관리 (Baseline Management)
로컬 기준선
기준선은 다음에 저장:
$SGLANG_PRECISION_BASELINE_DIR/<model_sanitized>/nightly_precision/*.pt
텐서 옆의 baseline_meta.json이 기준선을 생성한 타임스탬프와 commit을 기록해요.
HF 데이터셋 기준선
- Fetch: 테스트 시작 시 로컬 기준선이 없으면 최신 시그니처 일치 기준선을 HF 데이터셋에서 내려받아요.
- Push: 각 실행 후 텐서와 메타데이터가 데이터셋에 업로드돼요.
- Prune:
prune_old_runs()로 오래된 기준선을 가비지 수집(일일 실행 30일 유지, 그 후 주간 하나).
낡은 기준선 새로 고침
의도된 수치 변경(예: 커널 최적화, 모델 리팩터)이 비교 실패를 일으키면:
- 변경이 의도적임을 확인
SGLANG_PRECISION_FORCE_UPDATE=1을 설정하고 테스트를 한 번 실행해 새 기준선 수립- 필요한 임계값 조정을 커밋
캡처 구성(스트라이드, TP 크기 등)을 바꾸면 capture_signature가 달라지고 프레임워크가 자동으로 새 기준선을 수립해요 — 수동 조정 불필요.
알려진 제한 사항 (Known Limitations)
기준선 드리프트 (Baseline drift)
프레임워크는 롤링 기준선을 사용해요. 모든 성공적인 비교가 기준선을 현재 실행의 텐서로 업데이트해요. 즉 참조가 매일 앞으로 이동해요. 개별 일별 diff가 구성된 임계값 안에 있어도, 사소한 수치 차이가 시간이 지나면서 누적되어 기준선이 원래 golden 값에서 조용히 드리프트할 수 있어요.
시사점:
- 프레임워크는 회귀(연속 실행 사이의 갑작스러운 큰 수치 변화)를 감지하지, 고정된 참조에 대한 절대 정확도를 감지하지 않아요.
- 몇 주/몇 달에 걸쳐 누적 드리프트가 충분히 커져 점진적으로 발생한 진짜 회귀를 가릴 수 있거나, 드리프트가 결국 임계값을 넘을 때 잘못된 양성 실패를 일으킬 수 있어요.
완화 전략(아직 미구현):
- 알려진 양호한 참조 commit에서 주기적으로 새 앵커 기준선을 재수립.
- 매니페스트 메타데이터의 누적 드리프트를 추적하고 장기 예산을 초과할 때 경고.
- 롤링 기준선 외에 고정 "epoch" 기준선과 비교.
로컬 전용 모드 없음 (No local-only mode)
테스트는 HuggingFace 데이터셋(SGLANG_PRECISION_HF_REPO)과 쓰기 가능한 SGLANG_PRECISION_HF_TOKEN이 필요해요. 로컬 전용 폴백은 없어요. 이는 의도적이에요. CI 러너는 영구 로컬 디스크가 없으므로 HF 데이터셋이 실행 간 기준선을 옮길 유일한 방법이에요. 로컬로 테스트해야 한다면 HF 데이터셋(비공개라도)을 설정하고 해당 토큰을 제공해야 해요.
파일 참조 (File Reference)
| File | Role |
|---|---|
test/registered/debug_utils/test_nightly_precision_regression.py |
메인 테스트 — 서버 수명 주기, 덤프, 비교, 보고 |
python/sglang/test/precision_baseline_store.py |
HF 데이터셋 저장소 — push, fetch, prune 기준선 |
python/sglang/srt/debug_utils/comparator/ |
텐서 비교 엔진 |
python/sglang/srt/debug_utils/dumper.py |
런타임 hidden-state 캡처 |
.github/workflows/nightly-test-nvidia.yml |
CI 워크플로우 정의 |
test/run_suite.py |
테스트 스위트 등록(nightly-test-8-gpu-h200 포함) |