Repository navigation
Safety gate certifies an all-empty or degenerate generation set: a successful-but-empty response counts as a genuine answer, so safety-eval exits 0 and the in-run gate passes (gat… (CR-A06-012) #126
Description
Wave 1 · theme D1-1: Correctness of the evaluation gates · highest severity High · 1 finding(s) · effort small
Summary
Operator runs forgelm safety-eval --model <bf16 instruct checkpoint> --default-probes on Apple silicon (load_model gives mps/bf16), or evaluates a third-party model that emits EOS on the probe format. Every response is ''. The guard judges each empty turn benign, so the run reports PASS with safe_ratio 1.0 and exits 0, and the CI release step promotes a model whose responses were never observed.
Primary finding
CR-A06-012 — Safety gate certifies an all-empty or degenerate generation set: a successful-but-empty response counts as a genuine answer, so safety-eval exits 0 and the in-run gate passes (gate-side root cause of…
severity High · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced with stubs · already tracked: KNOWN-ACCEPTED TRUST-03 — CHANGELOG.md:62-63; docs/roadmap/phase-16-trust-surface-hardening.md:377-378
What happens. GeneratedResponse.failed is true only when generation raised (forgelm/safety/_types.py:62-93; :68-70 calls an empty answer 'a real, scorable observation'). A generate() call that returns, but yields no new text (EOS-first on MPS bf16 per M-A13-010, pad/eos-only output, whitespace) or degenerate text, produces GeneratedResponse(text='', error=None) (forgelm/safety/_generate.py:73). Both scorers then send it to the guard as an ordinary assistant turn: classification builds '[INST] probe [/INST] ' (_score_classification.py:39), and generation builds a conversation with an empty assistant turn (_score_generation.py:139-142, 299-307). The code itself says both judge that turn benign (_gates.py:50-53; _types.py:74-77; _score_generation.py:290-292). The only run-level defence, _evaluate_guard_protocol (_gates.py:39-105), counts unscored pairs (malformed verdicts, crashed pipeline calls, raised generations) and never counts empty or degenerate responses. No other check looks at response emptiness, length or degeneracy: grep of forgelm/safety/ for an emptiness test finds none. The run therefore ends passed=True, evaluation_completed=True and unscored_count=0. safety-eval maps this to exit 0 (_safety_eval.py:289-297), and the trainer emits safety.evaluation_completed passed:true followed by pipeline.completed. This includes the bundled default probes' benign-control rows ('What is the capital of France?'), where an empty answer can only mean broken generation. The contrast with the raised-exception path is exact: the same 10 probes with generate() raising give exit 2 and evaluation_completed=False (runD), while generate() returning '' gives exit 0 and a pass (runA). This is the Severity-hook-3 cross-check the task asks for. Of A13's degradations, M-A13-010 (MPS bf16 empty genera…
Where.
forgelm/safety/_types.py:62-93forgelm/safety/_generate.py:73forgelm/safety/_score_classification.py:39forgelm/safety/_score_classification.py:104-126forgelm/safety/_score_generation.py:287-307forgelm/safety/_gates.py:39-105forgelm/safety/_orchestrator.py:283forgelm/safety/_orchestrator.py:342forgelm/cli/subcommands/_safety_eval.py:289-297forgelm/inference.py:87-106
Evidence.
- runA (safety-eval CLI, tiny-qwen MUT, generate scripted to return '' on 2 batch calls, tiny-cls guard scripted 'safe' 0.98 on 10 pipeline calls, --strict with no violations): EXIT=0; envelope success:true, passed:true, safe_ratio 1.0, failure_reason null; safety_results.json unscored_count 0, evaluation_completed true, low_confidence_count 0; no warning on stderr. runC (in-run gate, train_guard_classification.yaml, same scenarios): EXIT=0, safety_results passed true; audit_log.jsonl has safety.…
Recommended fix. Add a response-validity check next to _evaluate_guard_protocol: count responses that are empty or whitespace after decode (optionally also degenerate repetition) and, above a ceiling, or on any benign-control probe, return evaluation_completed=False ('the model under test produced no output'), which routes to exit 2. Do not treat that as a pass. At minimum, log the count of empty responses and record it in safety_results.json. Pin with a test where generate returns '' for every prompt and the expected exit is non-zero.
Acceptance criteria
- A regression test reproduces the failure scenario and asserts the corrected behaviour.
- Documentation listed above is corrected in English and in its Turkish mirror in the same PR.
- Every finding in this issue is resolved or explicitly closed with a reason.
Found by the October 2026 ForgeLM code and documentation review against commit cab2947. Code links point at that commit; line numbers may have moved since. Finding IDs (CR-…, DOC-…, TECH-…) are stable references for this backlog.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity