初始化项目,由ModelHub XC社区提供模型
Model: jepetolee/Qwen3-4B-AMQ3-Math-SFT Source: Original Platform
This commit is contained in:
179
vllm_repetition_abort.py
Normal file
179
vllm_repetition_abort.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""과도 반복(폭주) 롤아웃 조기 종료용 vLLM V1 커스텀 logits processor.
|
||||
|
||||
배경: long-CoT eval 실측에서 롤아웃의 ~35%가 20480 캡까지 도달하며, 상당수가
|
||||
n-gram 반복 루프(어차피 reward에서 오답 처리됨)다. 이들이 가장 비싼(긴 컨텍스트)
|
||||
디코드 토큰을 소모한다.
|
||||
|
||||
동작: 요청별로 생성 토큰 목록(vLLM이 라이브 참조로 제공)을 감시하다가
|
||||
n-gram 반복 비율이 임계 이상이면 해당 요청의 로짓을 EOS만 남기고 -inf로 마스킹
|
||||
→ 그 요청만 즉시 종료. **트리거 전에는 로짓을 전혀 건드리지 않으므로 정상
|
||||
롤아웃의 출력은 비트 단위로 동일하다** (출력 불변 원칙 하에 폭주 커팅만 승인됨,
|
||||
2026-07-09 결정). 트리거된 롤아웃은 짧게 잘린 채 수집되고 기존 repetition
|
||||
필터/reward가 오답 처리한다.
|
||||
|
||||
사용법:
|
||||
1) 엔진: vllm_kwargs.logits_processors: ["ralo.vllm_repetition_abort:RepetitionEosLogitsProcessor"]
|
||||
2) 요청: SamplingParams.extra_args = {"repetition_abort": {
|
||||
"eos_token_id": <int>, # 필수 — 강제할 EOS
|
||||
"ngram": 7, # n-gram 크기 (dapo 필터와 동일 계열)
|
||||
"threshold": 0.9, # 반복 비율 임계 (필터 0.8보다 보수적 기본값 —
|
||||
# 출력이 실제로 잘리는 개입이므로)
|
||||
"min_tokens": 2048, # 이 길이 전에는 검사하지 않음
|
||||
"check_interval": 512, # 검사 주기 (토큰)
|
||||
}}
|
||||
extra_args에 repetition_abort가 없는 요청은 완전히 무시된다.
|
||||
|
||||
주의 (2026-07-16 실측): vLLM V1의 async scheduling(기본 자동 활성화)에서는 워커가
|
||||
output_tok_ids에 실제 토큰 대신 -1 플레이스홀더를 채워 이 프로세서가 무력화된다.
|
||||
반드시 엔진에 `async_scheduling=False`를 함께 전달할 것 (think_format·boxed_eos 공통).
|
||||
"""
|
||||
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import torch
|
||||
|
||||
try:
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.sample.logits_processor import BatchUpdate, LogitsProcessor
|
||||
from vllm.v1.sample.logits_processor.builtin import process_dict_updates
|
||||
_VLLM_OK = True
|
||||
except ImportError: # vLLM 미설치 환경(트레이너 전용 노드 등)에서도 임포트 가능하게
|
||||
_VLLM_OK = False
|
||||
LogitsProcessor = object # type: ignore
|
||||
BatchUpdate = None # type: ignore
|
||||
|
||||
|
||||
def ngram_repetition_ratio(token_ids: Sequence[int], n: int) -> float:
|
||||
"""반복 n-gram에 속하는 "위치"의 비율 (0..1).
|
||||
|
||||
주의: dapo/sampler.py의 필터 함수와 공식이 다르다. 필터는 "2회 이상 나온 고유
|
||||
n-gram 개수 ÷ 위치 수"인데, 이 정의는 순수 반복 루프에서 오히려 0에 수렴한다
|
||||
(고유 n-gram이 몇 개 안 되므로) — 재보정 주석의 실측치(정상 max 0.58, 폭주
|
||||
0.975)와 부합하는 것은 여기 구현한 "반복 n-gram이 차지하는 위치 비율" 쪽이다.
|
||||
순수 루프 → ~1.0, 무반복 텍스트 → 0.0.
|
||||
"""
|
||||
if n <= 0 or len(token_ids) < n * 2:
|
||||
return 0.0
|
||||
counts: dict = {}
|
||||
total = 0
|
||||
for i in range(len(token_ids) - n + 1):
|
||||
total += 1
|
||||
key = tuple(token_ids[i : i + n])
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
if total == 0:
|
||||
return 0.0
|
||||
repeated_positions = sum(c for c in counts.values() if c > 1)
|
||||
return repeated_positions / max(total, 1)
|
||||
|
||||
|
||||
class _ReqState:
|
||||
__slots__ = ("out", "eos", "ngram", "threshold", "interval", "next_check", "triggered")
|
||||
|
||||
def __init__(self, out, eos, ngram, threshold, min_tokens, interval):
|
||||
self.out = out # vLLM이 넘겨주는 라이브 출력 토큰 리스트 참조
|
||||
self.eos = int(eos)
|
||||
self.ngram = int(ngram)
|
||||
self.threshold = float(threshold)
|
||||
self.interval = max(1, int(interval))
|
||||
self.next_check = max(1, int(min_tokens))
|
||||
self.triggered = False
|
||||
|
||||
def should_trigger(self) -> bool:
|
||||
"""검사 시점 도달 시 반복 비율 평가. 트리거되면 True (한 번만)."""
|
||||
if self.triggered:
|
||||
return False
|
||||
if len(self.out) < self.next_check:
|
||||
return False
|
||||
ratio = ngram_repetition_ratio(self.out, self.ngram)
|
||||
if ratio >= self.threshold:
|
||||
self.triggered = True
|
||||
return True
|
||||
self.next_check = len(self.out) + self.interval
|
||||
return False
|
||||
|
||||
|
||||
class RepetitionEosLogitsProcessor(LogitsProcessor):
|
||||
"""반복 폭주 감지 시 해당 요청 로짓을 EOS 단일화 — 다른 요청/트리거 전 요청은 무변경."""
|
||||
|
||||
def __init__(self, vllm_config, device: torch.device, is_pin_memory: bool):
|
||||
if not _VLLM_OK:
|
||||
raise RuntimeError("vLLM V1 logits processor API를 찾을 수 없음")
|
||||
self.device = device
|
||||
self.pin_memory = is_pin_memory
|
||||
self.states: dict[int, _ReqState] = {}
|
||||
# 트리거된 (배치 인덱스, eos id) 마스크 텐서
|
||||
self._rows = self._tensor([], torch.int64)
|
||||
self._eos = self._tensor([], torch.int64)
|
||||
self._have_triggered = False
|
||||
|
||||
def _tensor(self, data, dtype):
|
||||
return torch.tensor(data, device="cpu", dtype=dtype, pin_memory=self.pin_memory).to(
|
||||
device=self.device, non_blocking=True
|
||||
)
|
||||
|
||||
def is_argmax_invariant(self) -> bool:
|
||||
return False # 트리거 시 argmax를 EOS로 바꾼다
|
||||
|
||||
@staticmethod
|
||||
def add_request(params: "SamplingParams", _prompt, output_tok_ids) -> Optional[_ReqState]:
|
||||
cfg = (getattr(params, "extra_args", None) or {}).get("repetition_abort")
|
||||
if not cfg or cfg.get("eos_token_id") is None:
|
||||
return None
|
||||
return _ReqState(
|
||||
out=output_tok_ids,
|
||||
eos=cfg["eos_token_id"],
|
||||
ngram=cfg.get("ngram", 7),
|
||||
threshold=cfg.get("threshold", 0.9),
|
||||
min_tokens=cfg.get("min_tokens", 2048),
|
||||
interval=cfg.get("check_interval", 512),
|
||||
)
|
||||
|
||||
def update_state(self, batch_update: "BatchUpdate | None") -> None:
|
||||
changed = process_dict_updates(self.states, batch_update, self.add_request)
|
||||
|
||||
newly = False
|
||||
for state in self.states.values():
|
||||
if state.should_trigger():
|
||||
newly = True
|
||||
|
||||
if changed or newly:
|
||||
rows, eos = [], []
|
||||
for idx, state in self.states.items():
|
||||
if state.triggered:
|
||||
rows.append(idx)
|
||||
eos.append(state.eos)
|
||||
self._rows = self._tensor(rows, torch.int64)
|
||||
self._eos = self._tensor(eos, torch.int64)
|
||||
self._have_triggered = bool(rows)
|
||||
if newly:
|
||||
try:
|
||||
print(f"[RepetitionAbort] {len(rows)} request(s) forced to EOS "
|
||||
f"(runaway n-gram repetition)", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def apply(self, logits: torch.Tensor) -> torch.Tensor:
|
||||
if self._have_triggered and self._rows.numel() > 0:
|
||||
# 트리거 행 전체 -inf 후 EOS만 0 → softmax에서 EOS 확률 1
|
||||
logits[self._rows] = float("-inf")
|
||||
logits[self._rows, self._eos] = 0.0
|
||||
return logits
|
||||
|
||||
|
||||
def build_repetition_abort_extra_args(algo_cfg: dict, eos_token_id) -> Optional[dict]:
|
||||
"""dapo_kwargs/lrs_kwargs의 repetition_abort 블록 → SamplingParams.extra_args.
|
||||
|
||||
블록이 없거나 enabled가 아니면 None (프로세서가 해당 요청을 완전히 무시).
|
||||
"""
|
||||
cfg = (algo_cfg or {}).get("repetition_abort") or {}
|
||||
if not cfg.get("enabled", False) or eos_token_id is None:
|
||||
return None
|
||||
return {
|
||||
"repetition_abort": {
|
||||
"eos_token_id": int(eos_token_id),
|
||||
"ngram": int(cfg.get("ngram", 7)),
|
||||
"threshold": float(cfg.get("threshold", 0.9)),
|
||||
"min_tokens": int(cfg.get("min_tokens", 2048)),
|
||||
"check_interval": int(cfg.get("check_interval", 512)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user