Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,17 +1016,22 @@ def run(
log_first: bool = False,
filter_cand: bool = True,
verbose: bool = True,
random_seed: int = 42,
) -> tuple[str, float, int]:
"""
Run iterative optimization.

Returns:
tuple[str, float, int]: The final control, loss, and step count.
"""
py_rng = random.Random(random_seed)
models = getattr(self, "models", None)
device = models[0].device if models else "cpu"
self._torch_gen = torch.Generator(device=device).manual_seed(random_seed)

def acceptance_probability(e: float, e_prime: float, k: int) -> bool:
temperature = max(1 - float(k + 1) / (n_steps + anneal_from), 1.0e-7)
return e_prime < e or math.exp(-(e_prime - e) / temperature) >= random.random()
return e_prime < e or math.exp(-(e_prime - e) / temperature) >= py_rng.random()

if target_weight is None:

Expand Down Expand Up @@ -1400,6 +1405,7 @@ def run(
stop_on_success: bool = True,
verbose: bool = True,
filter_cand: bool = True,
random_seed: int = 42,
) -> tuple[str, int]:
"""
Execute the progressive multi-prompt attack.
Expand Down Expand Up @@ -1431,6 +1437,8 @@ def run(
Whether to print verbose output (default is True)
filter_cand (bool, optional):
Whether to filter candidates whose lengths changed after re-tokenization (default is True)
random_seed (int, optional):
Seed for deterministic random number generation (default is 42)

Returns:
tuple[str, int]: The final control suffix and completed step count.
Expand Down Expand Up @@ -1499,6 +1507,7 @@ def run(
test_steps=test_steps,
filter_cand=filter_cand,
verbose=verbose,
random_seed=random_seed,
)
control, inner_loss, inner_steps = inner_result
schedule.loss = inner_loss
Expand Down Expand Up @@ -1656,6 +1665,7 @@ def run(
stop_on_success: bool = True,
verbose: bool = True,
filter_cand: bool = True,
random_seed: int = 42,
) -> tuple[str, int]:
"""
Execute the individual-prompt attack.
Expand Down Expand Up @@ -1687,6 +1697,8 @@ def run(
Whether to print verbose output (default is True)
filter_cand (bool, optional):
Whether to filter candidates (default is True)
random_seed (int, optional):
Seed for deterministic random number generation (default is 42)

Returns:
tuple[str, int]: The final control suffix and configured step count.
Expand Down Expand Up @@ -1741,6 +1753,7 @@ def run(
log_first=True,
filter_cand=filter_cand,
verbose=verbose,
random_seed=random_seed,
)

return self.control, n_steps
Expand Down
28 changes: 18 additions & 10 deletions pyrit/executor/promptgen/gcg/attack/gcg/gcg_attack.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def sample_control(
topk: int = 256,
temp: float = 1.0,
allow_non_ascii: bool = True,
torch_generator: torch.Generator | None = None,
) -> torch.Tensor:
"""
Sample new control token candidates based on gradients.
Expand All @@ -114,6 +115,7 @@ def sample_control(
topk (int): Number of top gradient positions to sample from. Defaults to 256.
temp (float): Temperature for sampling. Currently unused but kept for API compatibility. Defaults to 1.0.
allow_non_ascii (bool): Whether to allow non-ASCII tokens. Defaults to True.
torch_generator (torch.Generator | None): Optional generator for deterministic sampling.

Returns:
torch.Tensor: Batch of new candidate control token sequences.
Expand All @@ -127,7 +129,9 @@ def sample_control(
torch.int64
)
new_token_val = torch.gather(
top_indices[new_token_pos], 1, torch.randint(0, topk, (batch_size, 1), device=grad.device)
top_indices[new_token_pos],
1,
torch.randint(0, topk, (batch_size, 1), device=grad.device, generator=torch_generator),
)
return original_control_toks.scatter_(1, new_token_pos.unsqueeze(-1), new_token_val)

Expand Down Expand Up @@ -199,15 +203,19 @@ def _sample_control_candidates(
) -> torch.Tensor:
sampler = self._resolve_sampling()
prompt_manager = self.prompts[worker_index]
return sampler.sample_candidates(
gradient=gradient,
control_tokens=prompt_manager.control_toks,
batch_size=batch_size,
top_k=topk,
temperature=temp,
allow_non_ascii=allow_non_ascii,
non_ascii_tokens=prompt_manager.disallowed_toks,
)
torch_gen: torch.Generator | None = getattr(self, "_torch_gen", None)
kwargs: dict[str, Any] = {
"gradient": gradient,
"control_tokens": prompt_manager.control_toks,
"batch_size": batch_size,
"top_k": topk,
"temperature": temp,
"allow_non_ascii": allow_non_ascii,
"non_ascii_tokens": prompt_manager.disallowed_toks,
}
if torch_gen is not None:
kwargs["torch_generator"] = torch_gen
return sampler.sample_candidates(**kwargs)

def _filter_control_candidates(
self,
Expand Down
5 changes: 4 additions & 1 deletion pyrit/executor/promptgen/gcg/default_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def sample_candidates(
temperature: float,
allow_non_ascii: bool,
non_ascii_tokens: torch.Tensor,
torch_generator: torch.Generator | None = None,
) -> torch.Tensor:
"""
Sample ``batch_size`` candidate suffix token sequences.
Expand All @@ -79,6 +80,8 @@ def sample_candidates(
the top-k.
non_ascii_tokens (torch.Tensor): Token ids to exclude when
``allow_non_ascii`` is False.
torch_generator (torch.Generator | None): Optional generator for
deterministic sampling.

Returns:
torch.Tensor: Candidate suffix token sequences with shape
Expand All @@ -99,7 +102,7 @@ def sample_candidates(
new_token_val = torch.gather(
top_indices[new_token_pos],
1,
torch.randint(0, top_k, (batch_size, 1), device=gradient.device),
torch.randint(0, top_k, (batch_size, 1), device=gradient.device, generator=torch_generator),
)
return original_control_tokens.scatter_(1, new_token_pos.unsqueeze(-1), new_token_val)

Expand Down
4 changes: 4 additions & 0 deletions pyrit/executor/promptgen/gcg/extension_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def sample_candidates(
temperature: float,
allow_non_ascii: bool,
non_ascii_tokens: torch.Tensor,
torch_generator: torch.Generator | None = None,
) -> torch.Tensor:
"""
Sample ``batch_size`` candidate suffix token sequences.
Expand All @@ -101,6 +102,9 @@ def sample_candidates(
non_ascii_tokens (torch.Tensor): Token ids to exclude when
``allow_non_ascii`` is False, shape ``(num_disallowed,)``
and integer dtype.
torch_generator (torch.Generator | None): Optional random number
generator for deterministic sampling. When provided, all
random tensor operations should use this generator.

Returns:
torch.Tensor: Candidate suffix token sequences with shape
Expand Down
10 changes: 8 additions & 2 deletions pyrit/executor/promptgen/gcg/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,11 @@ async def _setup_async(self, *, context: GCGContext) -> None:
self._ensure_spawn_start_method()
context.memory_labels = combine_dict({}, context.memory_labels)

np_rng = np.random.default_rng(self._algorithm.random_seed)
context.targets, context.test_targets = self._apply_target_augmentation(
train_targets=context.targets,
test_targets=context.test_targets,
np_rng=np_rng,
)

log_gpu_memory(step=0)
Expand Down Expand Up @@ -318,6 +320,7 @@ async def _perform_async(self, *, context: GCGContext) -> GCGResult:
verbose=self._output.verbose,
filter_cand=self._algorithm.filter_cand,
allow_non_ascii=self._algorithm.allow_non_ascii,
random_seed=self._algorithm.random_seed,
)

return self._read_result(logfile_path=context.logfile_path, memory_labels=context.memory_labels)
Expand Down Expand Up @@ -381,22 +384,25 @@ def _apply_target_augmentation(
*,
train_targets: list[str],
test_targets: list[str],
np_rng: np.random.Generator | None = None,
) -> tuple[list[str], list[str]]:
"""
Randomly substitute equivalent target phrasings for diversity.

Returns:
tuple[list[str], list[str]]: Augmented training and held-out targets.
"""
if np_rng is None:
np_rng = np.random.default_rng()

def _shorten(s: str) -> str:
return s.replace("Sure, h", "H")

def _contract(s: str) -> str:
return s.replace("Sure, here is", "Sure, here's")

train_targets = [_shorten(t) if np.random.random() < 0.5 else _contract(t) for t in train_targets]
test_targets = [_shorten(t) if np.random.random() < 0.5 else _contract(t) for t in test_targets]
train_targets = [_shorten(t) if np_rng.random() < 0.5 else _contract(t) for t in train_targets]
test_targets = [_shorten(t) if np_rng.random() < 0.5 else _contract(t) for t in test_targets]
return train_targets, test_targets

def _to_attack_params(self, *, context: GCGContext) -> Any:
Expand Down
Loading