loop_abort.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """Stop a semantic generation loop before it fills the completion cap.
  2. VIS-04 (racing-animation HTML) streamed 714k tokens / ~2.6 h. SGLang's
  3. ``--watchdog-timeout`` only fires when a step hangs; a live token stream is
  4. healthy as far as the GPU watchdog is concerned. The 2× EXL3 recipe has no
  5. decode-side loop detector either.
  6. This hook wraps ``Req.update_finish_state`` and finishes the request with
  7. OpenAI ``finish_reason=stop`` / ``matched=repetition`` when:
  8. * the last ``repeats`` copies of an n-gram of length ``ngram``..``ngram_max``
  9. are identical (keeps one copy of the unit), or
  10. * the same token is emitted ``identical`` times in a row, or
  11. * the same decoded line (min ``line_min`` chars) appears ``line_repeats``
  12. times at the end of a short decode window.
  13. Defaults catch a 32-token cycle after four copies (~128 tokens, a few seconds
  14. at 38 tok/s) without tripping on short HTML/CSS punctuation. Set
  15. ``DSV41_LOOP_ABORT=0`` to disable.
  16. """
  17. import logging
  18. import os
  19. from types import SimpleNamespace
  20. logger = logging.getLogger(__name__)
  21. DEFAULT_ABORT = 1
  22. DEFAULT_NGRAM = 32
  23. DEFAULT_NGRAM_MAX = 256
  24. DEFAULT_REPEATS = 4
  25. DEFAULT_IDENTICAL = 64
  26. DEFAULT_LINE_REPEATS = 8
  27. DEFAULT_LINE_MIN = 16
  28. LINE_WINDOW_TOKENS = 512
  29. MATCHED_STOP = 'repetition'
  30. def _env_int(name, default):
  31. raw = os.environ.get(name)
  32. if raw is None or not str(raw).strip():
  33. return default
  34. try:
  35. return int(str(raw).strip())
  36. except ValueError:
  37. logger.warning('%s=%r is not an int; using %s', name, raw, default)
  38. return default
  39. def load_config():
  40. abort = _env_int('DSV41_LOOP_ABORT', DEFAULT_ABORT)
  41. ngram = _env_int('DSV41_LOOP_NGRAM', DEFAULT_NGRAM)
  42. ngram_max = _env_int('DSV41_LOOP_NGRAM_MAX', DEFAULT_NGRAM_MAX)
  43. repeats = _env_int('DSV41_LOOP_REPEATS', DEFAULT_REPEATS)
  44. identical = _env_int('DSV41_LOOP_IDENTICAL', DEFAULT_IDENTICAL)
  45. line_repeats = _env_int('DSV41_LOOP_LINE_REPEATS', DEFAULT_LINE_REPEATS)
  46. line_min = _env_int('DSV41_LOOP_LINE_MIN', DEFAULT_LINE_MIN)
  47. if ngram_max < ngram:
  48. ngram_max = ngram
  49. ngram_on = ngram > 0 and repeats > 0
  50. enabled = abort > 0 and (ngram_on or identical > 0 or line_repeats > 0)
  51. return SimpleNamespace(
  52. abort=abort,
  53. ngram=ngram,
  54. ngram_max=ngram_max,
  55. repeats=repeats,
  56. identical=identical,
  57. line_repeats=line_repeats,
  58. line_min=line_min,
  59. enabled=enabled,
  60. )
  61. def repeated_line_suffix(text, repeats, min_chars):
  62. """True when the last ``repeats`` non-empty lines are identical and long enough."""
  63. if not text or repeats <= 0 or min_chars <= 0:
  64. return False
  65. lines = [ln.rstrip() for ln in text.splitlines() if ln.strip()]
  66. if len(lines) < repeats:
  67. return False
  68. last = lines[-1]
  69. if len(last) < min_chars:
  70. return False
  71. return all(ln == last for ln in lines[-repeats:])
  72. def detect_repetition(ids, text=None, cfg=None):
  73. """Return ``(matched, finished_len)`` or ``None``.
  74. ``finished_len`` is the prefix to keep: one copy of a cycling n-gram, or
  75. one token of an identical run. Line hits do not trim (no token boundary).
  76. """
  77. if cfg is None:
  78. cfg = load_config()
  79. if not cfg.enabled:
  80. return None
  81. n = len(ids)
  82. if n == 0:
  83. return None
  84. if cfg.identical > 0 and n >= cfg.identical:
  85. last = ids[-1]
  86. run = 1
  87. i = n - 2
  88. while i >= 0 and ids[i] == last:
  89. run += 1
  90. i -= 1
  91. if run >= cfg.identical:
  92. return MATCHED_STOP, n - run + 1
  93. if cfg.ngram > 0 and cfg.repeats > 0:
  94. max_p = min(cfg.ngram_max, n // cfg.repeats)
  95. for period in range(cfg.ngram, max_p + 1):
  96. unit = ids[-period:]
  97. hit = True
  98. for r in range(1, cfg.repeats):
  99. start = n - (r + 1) * period
  100. end = n - r * period
  101. if ids[start:end] != unit:
  102. hit = False
  103. break
  104. if hit:
  105. extra = (cfg.repeats - 1) * period
  106. return MATCHED_STOP, n - extra
  107. if text is not None and repeated_line_suffix(text, cfg.line_repeats, cfg.line_min):
  108. return MATCHED_STOP, n
  109. return None
  110. def _decode_tail(req, ids):
  111. tok = getattr(req, 'tokenizer', None)
  112. if tok is None or not ids:
  113. return None
  114. tail = ids[-LINE_WINDOW_TOKENS:]
  115. try:
  116. return tok.decode(list(tail), skip_special_tokens=False)
  117. except Exception as exc:
  118. logger.warning('DSV41 loop abort: tokenizer.decode failed: %s', exc)
  119. return None
  120. def install(module):
  121. """Wrap ``Req.update_finish_state`` with a repetition stop."""
  122. cfg = load_config()
  123. if not cfg.enabled:
  124. logger.warning('DSV41 loop abort disabled (DSV41_LOOP_ABORT=%s)', cfg.abort)
  125. return
  126. cls = module.Req
  127. finish_cls = module.FINISH_MATCHED_STR
  128. original = cls.update_finish_state
  129. def update_finish_state(self, new_accepted_len=1):
  130. original(self, new_accepted_len)
  131. if self.finished():
  132. return
  133. ids = self.output_ids
  134. text = None
  135. if cfg.line_repeats > 0:
  136. text = _decode_tail(self, ids)
  137. hit = detect_repetition(ids, text=text, cfg=cfg)
  138. if hit is None:
  139. return
  140. matched, finished_len = hit
  141. self.finished_reason = finish_cls(matched=matched)
  142. self.finished_len = finished_len
  143. logger.warning(
  144. 'DSV41 loop abort rid=%s matched=%s output_tokens=%s kept=%s',
  145. getattr(self, 'rid', None), matched, len(ids), finished_len)
  146. cls.update_finish_state = update_finish_state
  147. logger.warning(
  148. 'DSV41 loop abort: ngram=%s-%s x%s, identical=%s, line_repeats=%s '
  149. '(min %s chars); DSV41_LOOP_ABORT=0 disables',
  150. cfg.ngram, cfg.ngram_max, cfg.repeats, cfg.identical,
  151. cfg.line_repeats, cfg.line_min)