Fix eval harness: honour --max-new-tokens, exclude truncation from syntax score, fall back across dataset splits
Browse files- eval_securecoder.py +37 -10
eval_securecoder.py
CHANGED
|
@@ -79,7 +79,7 @@ def parse_args() -> argparse.Namespace:
|
|
| 79 |
p.add_argument("--tool-prompts", type=int, default=80)
|
| 80 |
p.add_argument("--code-prompts", type=int, default=15)
|
| 81 |
p.add_argument("--upload-repo", default="Taimwe/securecoder-30b-pro")
|
| 82 |
-
p.add_argument("--max-new-tokens", type=int, default=
|
| 83 |
p.add_argument("--seed", type=int, default=3407)
|
| 84 |
return p.parse_args()
|
| 85 |
|
|
@@ -297,9 +297,15 @@ def eval_code_sanity(tokenizer, model, args) -> dict:
|
|
| 297 |
tokenize=False, add_generation_prompt=True,
|
| 298 |
)
|
| 299 |
ids = tokenizer(text, return_tensors="pt", add_special_tokens=False).input_ids.to(model.device)
|
|
|
|
| 300 |
with torch.no_grad():
|
| 301 |
-
generated = model.generate(ids, max_new_tokens=
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
except Exception as exc:
|
| 304 |
out.append({"prompt": prompt[:60], "error": repr(exc)[:120]})
|
| 305 |
continue
|
|
@@ -336,25 +342,46 @@ def eval_code_sanity(tokenizer, model, args) -> dict:
|
|
| 336 |
'prompt': prompt[:60],
|
| 337 |
'ast_ok': parsed,
|
| 338 |
'compile_ok': compiles,
|
|
|
|
| 339 |
'reply_first_160': reply[:160],
|
| 340 |
})
|
| 341 |
if (i + 1) % 5 == 0:
|
| 342 |
log.info(" code sanity: %d/%d", i + 1, len(prompts))
|
| 343 |
n = len(out)
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
return {"section": "code_sanity", "n_prompts": n,
|
| 347 |
-
"
|
|
|
|
|
|
|
|
|
|
| 348 |
"details": out}
|
| 349 |
|
| 350 |
|
| 351 |
|
| 352 |
def eval_security_mcq(tokenizer, model, n_questions: int = 25) -> dict:
|
| 353 |
import torch
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
rows = rows[:n_questions]
|
| 359 |
if not rows:
|
| 360 |
return {"section": "security_mcq", "skipped": True, "reason": "no rows"}
|
|
|
|
| 79 |
p.add_argument("--tool-prompts", type=int, default=80)
|
| 80 |
p.add_argument("--code-prompts", type=int, default=15)
|
| 81 |
p.add_argument("--upload-repo", default="Taimwe/securecoder-30b-pro")
|
| 82 |
+
p.add_argument("--max-new-tokens", type=int, default=1024)
|
| 83 |
p.add_argument("--seed", type=int, default=3407)
|
| 84 |
return p.parse_args()
|
| 85 |
|
|
|
|
| 297 |
tokenize=False, add_generation_prompt=True,
|
| 298 |
)
|
| 299 |
ids = tokenizer(text, return_tensors="pt", add_special_tokens=False).input_ids.to(model.device)
|
| 300 |
+
max_new = int(getattr(args, "max_new_tokens", 384) or 384)
|
| 301 |
with torch.no_grad():
|
| 302 |
+
generated = model.generate(ids, max_new_tokens=max_new, do_sample=False, eos_token_id=tokenizer.eos_token_id, pad_token_id=tokenizer.eos_token_id)
|
| 303 |
+
new_ids = generated[0, ids.shape[-1]:]
|
| 304 |
+
# Hitting the token cap means TRUNCATED, not "invalid Python". The previous
|
| 305 |
+
# version scored truncation as a syntax error, which made a working model
|
| 306 |
+
# look like it could not code (ast_rate 6.7%).
|
| 307 |
+
truncated = int(len(new_ids) >= max_new)
|
| 308 |
+
reply = tokenizer.decode(new_ids, skip_special_tokens=True)
|
| 309 |
except Exception as exc:
|
| 310 |
out.append({"prompt": prompt[:60], "error": repr(exc)[:120]})
|
| 311 |
continue
|
|
|
|
| 342 |
'prompt': prompt[:60],
|
| 343 |
'ast_ok': parsed,
|
| 344 |
'compile_ok': compiles,
|
| 345 |
+
'truncated': truncated,
|
| 346 |
'reply_first_160': reply[:160],
|
| 347 |
})
|
| 348 |
if (i + 1) % 5 == 0:
|
| 349 |
log.info(" code sanity: %d/%d", i + 1, len(prompts))
|
| 350 |
n = len(out)
|
| 351 |
+
n_trunc = sum(1 for r in out if r.get("truncated"))
|
| 352 |
+
# Truncated replies are excluded from the syntax denominator, but if EVERY reply
|
| 353 |
+
# was truncated we still report over all of them rather than dividing by zero.
|
| 354 |
+
scored = [r for r in out if not r.get("truncated")] or out
|
| 355 |
+
ast_ok = sum(1 for r in scored if r.get("ast_ok"))
|
| 356 |
+
compile_ok = sum(1 for r in scored if r.get("compile_ok"))
|
| 357 |
return {"section": "code_sanity", "n_prompts": n,
|
| 358 |
+
"n_scored": len(scored), "n_truncated": n_trunc,
|
| 359 |
+
"truncated_rate": n_trunc / max(n, 1),
|
| 360 |
+
"ast_rate": ast_ok / max(len(scored), 1),
|
| 361 |
+
"compile_rate": compile_ok / max(len(scored), 1),
|
| 362 |
"details": out}
|
| 363 |
|
| 364 |
|
| 365 |
|
| 366 |
def eval_security_mcq(tokenizer, model, n_questions: int = 25) -> dict:
|
| 367 |
import torch
|
| 368 |
+
rows = None
|
| 369 |
+
last_exc = None
|
| 370 |
+
# CyberNative/CyberSecurityEval only publishes a "test" split. The old code asked
|
| 371 |
+
# for "train", raised ValueError, and the whole security section was skipped as
|
| 372 |
+
# "skipped" - which is why security capability was never measured.
|
| 373 |
+
for split in ("test", "train", "validation"):
|
| 374 |
+
try:
|
| 375 |
+
rows = _fetch_first_rows("CyberNative/CyberSecurityEval", None, split,
|
| 376 |
+
n_questions * 2)
|
| 377 |
+
except Exception as exc: # noqa: BLE001 - try the next split
|
| 378 |
+
last_exc = exc
|
| 379 |
+
rows = None
|
| 380 |
+
if rows:
|
| 381 |
+
break
|
| 382 |
+
if rows is None:
|
| 383 |
+
return {"section": "security_mcq", "error": repr(last_exc)[:200],
|
| 384 |
+
"skipped": True, "reason": "no usable split"}
|
| 385 |
rows = rows[:n_questions]
|
| 386 |
if not rows:
|
| 387 |
return {"section": "security_mcq", "skipped": True, "reason": "no rows"}
|