Repository navigation
Score Jailbreak 1.2 with the canonical 19-model reference calibration - #1701
RikiConrey wants to merge 5 commits into
Conversation
|
MLCommons CLA bot: |
| raise ValueError("Duplicate evaluation for a stacked prompt") | ||
| seen.add(uid) | ||
| row = form["by_prompt"][uid] | ||
| for field in ("hazard", "prompt_condition", "naive_prompt_uid"): |
There was a problem hiding this comment.
Is it possible to add some documentation somewhere that would help us (eng) troubleshoot these situations? The error message as written doesn't tell me where or how to start looking.
There was a problem hiding this comment.
Every error now names the SUT or prompt it is about, and the README next to the module has a table: message, where to look.
| estimates = {hazard: 0.0 for hazard in hazards} | ||
| count = len(hazards) | ||
|
|
||
| def gradient(hazard, candidate): |
There was a problem hiding this comment.
Does this need to be inside another function? It's hard to read and test this way.
There was a problem hiding this comment.
Thanks for this! Flattened. The gradient and the per-hazard solve are plain module-level functions now.
| PROMPT_SETS = SECURITY_1_1_JAILBREAK_PROMPT_SETS # pyright: ignore | ||
|
|
||
| def score_benchmark_run(self, benchmark_run, sut): | ||
| from modelbench.benchmark_score import SecurityPairedScore |
There was a problem hiding this comment.
What's the reason for inline imports like this?
There was a problem hiding this comment.
Module-level imports. The hook in benchmarks.py is one line.
| sut = make_sut(sut_uid) | ||
| benchmark_cls = benchmark_class_for(_BENCHMARK_PREFIXES["security"], version) | ||
| benchmark = benchmark_cls(locale, prompt_set, evaluator=evaluator) | ||
| if version == "1.2" and scoring_config is None: |
There was a problem hiding this comment.
The default version is now 1.2 from _SECURITY_VERSIONS[0], which requires the scoring config and scoring config sha. This breaks the existing invocation with default values and modelbench benchmark security --sut X fails when it used to work.
| VERSION = "1.1" | ||
| PROMPT_SETS = SECURITY_1_1_JAILBREAK_PROMPT_SETS # pyright: ignore | ||
|
|
||
| def score_benchmark_run(self, benchmark_run, sut): |
There was a problem hiding this comment.
Instead of having the version checks here, since there's a SecurityBenchmarkV1_2 class, I think it'd be better if the 1.2 functionality were implemented in SecurityBenchmarkV1_2.score_benchmark_run.
| @@ -0,0 +1,65 @@ | |||
| { | |||
There was a problem hiding this comment.
Why name a JSON file .contract instead of .json?
There was a problem hiding this comment.
Done. this was to align with our external docs.
| @@ -0,0 +1,112 @@ | |||
| Security paired continuous scoring | |||
There was a problem hiding this comment.
Why name a markdown file .contract instead of .md?
There was a problem hiding this comment.
Removed. Aligned with external docs.
| by_prompt[uid] = dict(row) | ||
| if not by_source or any(set(partners) != set(CONDITIONS) for partners in by_source.values()): | ||
| raise ValueError("Stacked form requires both naive and jailbreak for every source") | ||
| return {"prompt_uids": sorted(by_source), "by_prompt": by_prompt} |
There was a problem hiding this comment.
Is prompt_uids referring to the prompt_uid or the native_prompt_uid?
There was a problem hiding this comment.
Renamed naive_prompt_uids wherever it means the source prompt.
I remain pretty confused about these, actually. I'm copying them from the files I am getting from engineering, but the field names seem brittle, so TODO data store.
| test_item=item, sut_response=wrapped.sut_response, annotations={"security-v1.1": restored} | ||
| ) | ||
| assert isinstance(reloaded.annotations["security-v1.1"], ViolationProbabilityAnnotation) | ||
| test = object.__new__(PrivateSecurityJailbreakTestV1_1) |
There was a problem hiding this comment.
What is this? Why not use the constructor?
| "attack_type_id": "rot13" if condition == "jailbreak" else "", | ||
| } | ||
| ) | ||
| test = object.__new__(PrivateSecurityJailbreakTestV1_1) |
There was a problem hiding this comment.
What is this? Why not use the constructor?
There was a problem hiding this comment.
Major points:
- Don't change the CLI, if possible.
- Move more code into the external code directory
- What is security 1.2? I thought all this new violation probability work is a part of security 1.1.
- The new security modelgauge test code is extremely difficult to understand.
| @@ -0,0 +1,65 @@ | |||
| { | |||
There was a problem hiding this comment.
How are the contracts meant to be used? Why are they in the docs? This information doesn't feel like it pertains to modelbench users. It also looks extremely private? Should it be in a public repo?
| ) | ||
| PROFILE_EXCLUDED_DOMAINS = { | ||
| "all_hazards": frozenset(), | ||
| "exclude_cse_and_specialized_advice": frozenset({"cse", "spc_adv"}), |
There was a problem hiding this comment.
wait, should we be making changes to safety 1.2 if its already released?
There was a problem hiding this comment.
No, guidance has been moving, but now the guidance is we will make these changes the week after Candela launches, and I will PUBLISH scores from my side on Candela launch.
| } | ||
|
|
||
|
|
||
| class SecurityBenchmarkV1_2(SecurityBenchmarkV1_1): |
There was a problem hiding this comment.
I'm confused. I thought this work is just for security 1.1.
There was a problem hiding this comment.
You're right. It's Security 1.1. 1.2 is gone.
| if hasattr(annotation, "violation_probability"): | ||
| ann["violation_probability"] = annotation.violation_probability | ||
| context = item.test_item.context | ||
| ann.update(sut_uid=sut_uid, prompt_uid=item.source_id()) |
There was a problem hiding this comment.
why do we need to record this information?
| context = item.test_item.context | ||
| ann.update(sut_uid=sut_uid, prompt_uid=item.source_id()) | ||
| if isinstance(context, dict): | ||
| for field in ("naive_prompt_uid", "prompt_condition", "hazard"): |
There was a problem hiding this comment.
Why do we need to record naive_prompt_uid and prompt_condition? Also, it looks like hazard is already covered on line 257.
| ) | ||
| if not isinstance(response, dict): | ||
| raise ValueError(f"Unexpected response type: {type(response)}") | ||
| if request.annotator == SECURITY_ANNOTATOR_V1_1_UID and "violation_probability" in response: |
There was a problem hiding this comment.
Do we need the first part of this conditional? We might create more evaluator versions in the future that also use the violation probability format.
| if request.annotator == SECURITY_ANNOTATOR_V1_1_UID and "violation_probability" in response: | ||
| from modelbench.external_scoring.security_v_1_1_scoring import _probability | ||
|
|
||
| _probability(response) |
|
|
||
| with open(dependency_helper.get_local_path(self.prompt_set_file_base_name)) as handle: | ||
| rows = [row for row in csv.DictReader(handle) if row["locale"].lower() == self.locale] | ||
| self._is_stacked_form = any("prompt_condition" in row for row in rows) |
There was a problem hiding this comment.
what is a stacked form? and a prompt condition?
There was a problem hiding this comment.
So this is all the design work I did with Kurt. condition is naive OR jailbreak. stacked means that we put all those in one file so ids for the prompts in question are out of sync.
This matches what I gave to Kurt, but if he reconfigured, this will break. I can't see modellab.
| items = [] | ||
| for row in rows: | ||
| attack = None | ||
| if row["prompt_condition"] == "jailbreak": |
There was a problem hiding this comment.
Perhaps this should be a different method for the naive and jailbreak test objects?
There was a problem hiding this comment.
Kept as one test object. It is one file, and both rows have to go out for the pairing to work. The format sniffing that made it hard to read is gone.
After the prompts are sorted, no runner activity is different for naive and jailbreak.
| num_items: int | ||
|
|
||
|
|
||
| class SecurityContinuousTestResult(BaseModel): |
There was a problem hiding this comment.
This is what the security v1.1 tests return? Seems... useless?
There was a problem hiding this comment.
Is is! the runner carries the scores. AFAIK, the test object should return NULL. I think the reason it has stuff in is is legacy from when it was storing thresholded scores and created a SUT score after the run? TODO is to remove elements of this that look like scoring a SUT.
Security 1.1 scores each SUT from the evaluator's violation probabilities. The naive and jailbreak nonviolation scores use the same complete prompt pairs; more than 10% incomplete pairs in any hazard blocks publication before any record is written. The scorer, its score object, its own copy of the linked-hazard solver, and the difficulty and discrimination for all 3,250 source prompts (pinned by SHA-256) live in external_scoring. Outside it: the Security 1.1 test object reads the stacked prompt file and keeps the probability per response, the benchmark definition hands the run to the scorer in one line, the runner leaves probability judgments alone, the evaluator client builds a probability judgment when one is returned, and type declarations accept the second judgment type. No CLI changes. Security 1.2 is removed; safety_v_1_2_scoring.py is unchanged from main. The docs contract files are removed; the contract is a README next to the module. The parameters file is committed at the owner's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Security 1.1 scores each SUT from the evaluator's violation probabilities instead of yes/no labels. Both the naive and the jailbreak nonviolation score come from the same complete prompt pairs. If more than 10% of any hazard's pairs are incomplete, the run is blocked before any record is written.
New information today that the goal is to keep everything we can in "external scoring". This is now done. No CLI: that config is now committed.
Depends on Sugar PR 748.
🤖 Generated with Claude Code