Skip to content

Commit 85f9715

Browse files
committed
Minor fixes round
1 parent b130ceb commit 85f9715

40 files changed

Lines changed: 92 additions & 86 deletions

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ TROPT (Textual Trigger Optimization Toolbox) is a research platform for optimizi
2525

2626
```bash
2727
uv sync --all-extras # install in dev mode with all extras
28-
pre-commit install # install pre-commit hooks
2928
```
3029

3130
Always invoke tools via `uv run` (e.g. `uv run ruff check`, `uv run ty check`, `uv run pytest`).

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030

3131

3232
- ⚔️ **Red-team LLMs out of the box:** Craft jailbreaks and other LLM attacks with **30+ ready-to-run recipes** — spanning white- and black-box methods (GCG, BEAST, MAC, GASLITE, …) — each invocable in a single call, to evaluate model and defense robustness.
33-
- 🔁 **Extend to any NLP model:** Seamlessly port existing optimization schemes (e.g., LLM jailreabks) to any model (e.g., retrievers, classifiers, multimodal systems), or to novel tasks (e.g., new attack vectors, interpretability research).
33+
- 🔁 **Extend to any NLP model:** Seamlessly port existing optimization schemes (e.g., LLM jailbreaks) to any model (e.g., retrievers, classifiers, multimodal systems), or to novel tasks (e.g., new attack vectors, interpretability research).
3434
- 🧩 **Compose new optimization recipes:** Mix and match any optimizer (gradient-based, continuous-relaxation, black-box) with any loss (logits, embeddings, attention, activations, LM-as-judge) to create adaptive and novel optimization recipes in new domains.
35-
- 🔬 **Build new optimizers and losses:** Build **new optimizers** leveraging TROPT's standardized, lightweight optimizer implementation and its exetensive toolkit. Or, **customize loss** by only defining its core logic. TROPT **automatcally integrate** new optimizers and losses to with any model and recipe (including batching, trigger combination, gradients), avoiding annoying yet subtle boierplate.
36-
- 🛡️ **Reliable Benchmarking:** Run fair, reproducible comparisons of optimizers and their enhancements on shared infrastructure and a rich bank of optimziers, losses, etc.
35+
- 🔬 **Build new optimizers and losses:** Build **new optimizers** leveraging TROPT's standardized, lightweight optimizer implementation and its extensive toolkit. Or, **customize loss** by only defining its core logic. TROPT **automatically integrates** new optimizers and losses with any model and recipe (including batching, trigger combination, gradients), avoiding annoying yet subtle boilerplate.
36+
- 🛡️ **Reliable Benchmarking:** Run fair, reproducible comparisons of optimizers and their enhancements on shared infrastructure and a rich bank of optimizers, losses, etc.
3737

3838

3939
## 🚀 Getting Started
@@ -48,10 +48,9 @@ pip install tropt[all] # all optional extras (OpenAI, LiteLLM, tracking, ..
4848
For development, we use [uv](https://docs.astral.sh/uv/):
4949

5050
```bash
51-
git clone https://github.com/matanbt/tropt.git
51+
git clone https://github.com/matanbt/TROPT.git
5252
cd tropt
5353
uv sync --extra dev
54-
pre-commit install
5554
```
5655

5756
### Quick Start: Run a Recipe 🥗
@@ -130,7 +129,7 @@ You can help improve TROPT in the following two ways:
130129
## Intended Use
131130

132131
TROPT is built for defensive research:
133-
auditing, interpretabiliy, robustness evaluation,
132+
auditing, interpretability, robustness evaluation,
134133
and authorized red-teaming of NLP models.
135134
**Do not use TROPT to attack systems you don't own or to elicit harmful behaviors from deployed models in the wild.**
136135

@@ -143,6 +142,7 @@ If you find this package useful, please cite our paper as follows:
143142
```bibtex
144143
@misc{tropt2026,
145144
title = {TROPT: An Open Framework for Unifying and Advancing Discrete Text Optimization},
145+
author = {Ben-Tov, Matan and Sharif, Mahmood},
146146
year = {2026},
147147
howpublished = {\url{https://github.com/matanbt/TROPT}},
148148
}

skills/tropt/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ optimizer (declares model_requirements, drives the search)──┘
3737
Two design principles to keep in mind: *modularity* (each component swaps largely independently) and *backend–frontend separation* (tokenization, batching, gradient computation live in the model "backend"; losses and optimizers stay lightweight "frontend").
3838

3939
- **Model** (`tropt/model/`) — wraps the target model and exposes capabilities via **access mixins**. The optimizer declares which mixins it needs; `BaseOptimizer.__init__` validates the model satisfies them. Common mixins: `LossTokenAccessMixin` (grey-box loss from tokens), `GradientTokenAccessMixin` (white-box gradients), `LossTextAccessMixin` (black-box, text-only), `LogitsTokenAccessMixin`, `GradientEmbedAccessMixin`. Backends: `LMHFModel` (HF causal LMs — most mixins), `EncoderHFModel`, `EncoderOpenAIModel`, `LiteLLMModel`, `EncoderGeminiModel`.
40-
- **Loss** (`tropt/loss/`) — pure objective. Receives fields from `ModelOutput` / `ModelInput` / `MessageTargets` by **parameter-name matching** in its `__call__` signature (the one rule). Categories: `LogitBasedLoss`, `EmbeddingBasedLoss`, `TextBasedLoss`, `AttentionBasedLoss`, `HiddenStateBasedLoss`, `CombinedLoss`.
40+
- **Loss** (`tropt/loss/`) — pure objective. Receives fields from `ModelOutput` / `ModelInput` / `MessageTargets` by **parameter-name matching** in its `__call__` signature (the one rule). Categories: `PrefillBasedLoss`, `TriggerLogitBasedLoss`, `EmbeddingBasedLoss`, `TextBasedLoss`, `AttentionBasedLoss`, `HiddenStateBasedLoss`, `ClassificationBasedLoss`, `CombinedLoss`.
4141
- **Optimizer** (`tropt/optimizer/`) — the search algorithm. Self-contained, one file per optimizer (HuggingFace "Repeat Yourself"). Declares `model_requirements = (Mixin1, Mixin2)` at class level.
4242
- **Inputs & targets** — the user-supplied input template(s) carrying the `{{OPTIMIZED_TRIGGER}}` placeholder, plus a `Targets` dataclass holding per-template targets (`target_response_strs`, `target_vectors`, `target_directions`, `target_class_idx`, …). Each set field has length `n_templates`. Templates and targets are passed to `optimizer.optimize_trigger(templates=..., targets=...)`; the framework handles trigger/template combination internally.
4343

44-
The **Recipe Hub** (`tropt/recipe_hub/`) ships ~30 pre-wired (Model, Loss, Optimizer, defaults) combinations as one-call functions. That's almost always the right starting point.
44+
The **Recipe Hub** (`tropt/recipe_hub/`) ships ~38 pre-wired (Model, Loss, Optimizer, defaults) combinations as one-call functions. That's almost always the right starting point.
4545

4646
## 3. Install or update TROPT
4747

@@ -185,17 +185,17 @@ These apply across optimizers and recipes — surface them when relevant rather
185185
- **Missing `{{OPTIMIZED_TRIGGER}}`** in templates → optimizer raises. Every template must contain the placeholder string (exported as `tropt.common.OPTIMIZED_TRIGGER_PLACEHOLDER`).
186186
- **Mixin mismatch** — pairing a black-box `LiteLLMModel` with `GCGOptimizer` (which needs `GradientTokenAccessMixin`) fails at construction time, not silently. If the user wants a gradient-free attack on a black-box model, point them at `prs__andriushchenko2024` / `rs_emb` / `gcgp_blackbox__hayase2024`.
187187
- **`Targets` field length mismatch** — every set field must have length `n_templates`. Universal triggers across N templates need N targets (often repeated: `["Sure, here is"] * N`).
188-
- **Thinking-model target misalignment (Qwen3, etc.)** — these models prefix every emits a `<think>...</think>` block, so the affirmative target the optimizer chases (e.g. `"Sure, here's how:"`) never appears at position 0 of the generation. Prepend the empty thinking block to the target: `target = "<think>\n\n</think>\n\n" + "Sure, here's how:"`. Without this fix, prefill-CE losses chase a position the model will never write to.
188+
- **Thinking-model target misalignment (Qwen3, etc.)** — these models always emit a `<think>...</think>` block first, so the affirmative target the optimizer chases (e.g. `"Sure, here's how:"`) never appears at position 0 of the generation. Prepend the empty thinking block to the target: `target = "<think>\n\n</think>\n\n" + "Sure, here's how:"`. Without this fix, prefill-CE losses chase a position the model will never write to.
189189
- **Some losses impose model-construction requirements** beyond just the access mixins. Check the loss's docstring before pairing it with a recipe:
190190
- `AttentionBasedLoss` family (e.g. `AttentionEnhLoss`) needs the model loaded with `use_eager_attention=True` — FlashAttention/SDPA paths don't return attention weights. `LMHFModel(..., use_eager_attention=True)`.
191-
- `AttentionEnhLoss` with `dst=INPUT_AFTER` (and other suffix-position-dependent losses) requires the trigger to be a **true suffix** of the user message — strip any trailing punctuation after `{{OPTIMIZED_TRIGGER}}` in the template (`"... harmful thing. {{OPTIMIZED_TRIGGER}}"``"... harmful thing {{OPTIMIZED_TRIGGER}}"`).
191+
- `AttentionEnhLoss` with `dst_slc_name=SliceKey.INPUT_AFTER` (and other suffix-position-dependent losses) requires the trigger to be a **true suffix** of the user message — strip any trailing punctuation after `{{OPTIMIZED_TRIGGER}}` in the template (`"... harmful thing. {{OPTIMIZED_TRIGGER}}"``"... harmful thing {{OPTIMIZED_TRIGGER}}"`).
192192
- `TriggerPerplexityLoss` and other trigger-slice-dependent losses require `use_prefix_cache=False` on the model — prefix caching shifts the trigger slice's start position.
193193
- Hidden-state / activation losses (`SteeringActivationLoss`, etc.) often need pre-computed direction vectors (e.g. `compute_refusal_directions(model, n_samples=128)`) passed via `Targets(target_directions=...)`.
194194
- **Stop by budget, not just by `num_steps`** — TROPT optimizers support `optimizer.set_budget(BUDGET, metric="total_flops")` (white-box) and `set_budget(BUDGET, metric="total_tokens", scope="target")` (black-box / API), letting you cap compute or API cost directly instead of guessing a step count. Pair with `model.set_flop_counting("manual")` on the model. Far more reliable across optimizers with different per-step costs.
195-
- **Initial trigger init** — prefer `tropt.optimizer.utils.token_initializers.get_printable_random_trigger(trigger_len, tokenizer, blacklist_ids=TokenConstraints().get_blacklist_ids(tokenizer))` over hand-crafted `"! ! ! ! !"` strings. The hand-crafted form depends on the tokenizer treating `!` as a single token (not universal) and gives every position the same starting gradient, which hurts on some models.
195+
- **Initial trigger init** — prefer `tropt.optimizer.utils.token_initializers.get_printable_random_trigger(trigger_len, tokenizer=tokenizer, blacklist_ids=TokenConstraints().get_blacklist_ids(tokenizer))` over hand-crafted `"! ! ! ! !"` strings. The hand-crafted form depends on the tokenizer treating `!` as a single token (not universal) and gives every position the same starting gradient, which hurts on some models.
196196
- **Pass `TokenConstraints` to the optimizer, not just to trigger init** — without `token_constraints=`, optimizers may emit triggers with non-ASCII Unicode, model special tokens (`<|im_start|>`, `<bos>`, etc.), or other artifacts that break downstream evaluation, leak chat-template structure, or are trivially filterable. For red-teaming / robustness work the safe default is `TokenConstraints(disallow_non_ascii=True, disallow_special_tokens=True)`, passed both to the optimizer and to `get_blacklist_ids(tokenizer)` for trigger init. Recipe Hub recipes already do this; raw `Pattern B` compositions in user scripts often forget.
197197
- **Multi-model OOM** — when a recipe needs a *second* model (teacher for IRIS / FLRT distillation, proxy for PAL/RAL/QCG in black-box mode, utility LM for AdvDecoding/BeamSearch), do not let it co-reside with the victim. Standard pattern: load aux → generate / extract what you need → `del aux._model; del aux; gc.collect(); torch.cuda.empty_cache(); torch.cuda.synchronize()` → THEN load victim. Holding both on one GPU blows memory on most consumer setups.
198-
- **Black-box loss is not the same as white-box loss**`PrefillCELoss` reads `full_logits`, which an API/LiteLLM model doesn't expose. For API targets swap to `FirstTokenNLLLoss(target_token="Sure")` (or another text/first-token-based loss). Optimizer-side, the model itself also flips to `LiteLLMModel`, and only optimizers declaring `LossTextAccessMixin` in their `model_requirements` will accept it.
198+
- **Black-box loss is not the same as white-box loss**`PrefillCELoss` reads `prefill_response_logits` (target prefill), which an API/LiteLLM model can't provide. For API targets swap to `FirstTokenNLLLoss(target_token="Sure")` (or another text/first-token-based loss). Optimizer-side, the model itself also flips to `LiteLLMModel`, and only optimizers declaring `LossTextAccessMixin` in their `model_requirements` will accept it.
199199
- **Large models on a small machine** — don't suggest `meta-llama/Llama-3.1-8B-Instruct` for someone debugging locally on a laptop. Default to `google/gemma-3-270m-it` or `google/gemma-3-1b-it` for smoke tests; the recipe still reproduces, just with weaker results.
200200
- **Recipe naming**`foo__year2024` means it reproduces the paper's algorithm + hyperparameters. Bare `foo` means it's a variant or a setting-port. When recommending a recipe for paper reproduction, prefer the tagged form.
201201
- **WandB tracking is opt-in** — instantiate `WandbTracker()` and pass it to the optimizer; otherwise nothing logs. `LiveLossPlot` is the lighter local alternative.

tropt/loss/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class BaseLoss(ABC):
3939
"""Whether this loss requires autoregressive generation."""
4040

4141
require_hidden_states: ClassVar[bool] = False
42-
"""Whether this loss requires the model to provid the forward pass's hidden states."""
42+
"""Whether this loss requires the model to provide the forward pass's hidden states."""
4343

4444
require_attentions: ClassVar[bool] = False
4545
"""Whether this loss requires the model to return attention weights."""
@@ -128,7 +128,7 @@ def __call__(self, losses: Float[Tensor, "n_losses bsz"]) -> Float[Tensor, "bsz"
128128

129129
weights = self.weights.to(losses).unsqueeze(-1) # shape: (n_losses, 1)
130130
loss = losses * weights
131-
loss = loss.sum(dim=0) # recude over n_losses
131+
loss = loss.sum(dim=0) # reduce over n_losses
132132

133133
return loss # shape: (bsz,)
134134

tropt/loss/losses.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,7 @@ class AttentionEnhLoss(AttentionBasedLoss):
353353
Enable to instantiate the (different) losses from:
354354
https://arxiv.org/abs/2506.12880, https://arxiv.org/abs/2410.09040
355355
356-
Note that it requires setting `use_eager_attention=True` when loading the model (for explicit attention computations); also it
357-
is some slices are not supported when LM prefix caching is enabled, so it should be set to `use_prefix_cache=False` when loading the model.
356+
Note that it requires setting `use_eager_attention=True` when loading the model (for explicit attention computations); also, some slices are not supported when LM prefix caching is enabled, so set `use_prefix_cache=False` when loading the model.
358357
"""
359358

360359
targeted_layers: slice = slice(None)
@@ -367,7 +366,7 @@ def __call__(
367366
input_slices: dict[SliceKey, slice],
368367
) -> Float[Tensor, "bsz"]:
369368
if SliceKey.INPUT_AFTER in (self.src_slc_name, self.dst_slc_name):
370-
logger.debug("Note: `INPUT_AFTER` slice is currently only correct for LMs and on suffix attacks. If the usage is different, somethings may break, or worse -- be wrong.")
369+
logger.debug("Note: `INPUT_AFTER` slice is currently only correct for LMs and on suffix attacks. If the usage is different, something may break, or worse -- be wrong.")
371370
slc_src = input_slices.get(self.src_slc_name, slice(None))
372371
slc_dst = input_slices.get(self.dst_slc_name, slice(None))
373372

@@ -461,9 +460,10 @@ class SteeringActivationLoss(HiddenStateBasedLoss):
461460
Args:
462461
targeted_layers: Which layers to apply steering on (default: all layers)
463462
steer_away: Whether to minimize alignment instead of maximizing (default: False = steer towards)
464-
slc_name: Which token positions to apply steering on (default: "last_input_token")
463+
slc_name: Which token positions to apply steering on (default: "input_last_token")
465464
do_cosine_sim: Whether to use cosine similarity instead of dot product (default: False)
466465
apply_square: Whether to square the similarity scores (default: False)
466+
apply_abs: Whether to take the absolute value of the similarity scores (default: False).
467467
"""
468468

469469
targeted_layers: slice = slice(None)
@@ -483,7 +483,7 @@ def __call__(
483483
Compute steering loss by measuring cosine similarity between hidden states and target directions.
484484
485485
Args:
486-
output_hidden_states: Model hidden states from all layers and positions (bsz, n_layers, seq_len, d_model)
486+
full_hidden_states: Model hidden states from all layers and positions (bsz, n_layers, seq_len, d_model)
487487
target_directions: Direction vectors to align with (, d_model)
488488
input_slices: Position slices reflecting the input tokens (dict mapping slice names to slices)
489489

tropt/loss/resolution.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,14 @@ def resolve_and_compute_loss(
7373
7474
Examples:
7575
>>> from tropt.common import MessageTargets
76-
>>> # Encoder model with SimilarityLoss(output_embeddings, target_embeddings)
76+
>>> # Encoder model with SimilarityLoss(output_embeddings, target_vectors)
7777
>>> output = ModelOutput(output_embeddings=torch.randn(4, 768))
7878
>>> input_data = ModelInput(message_targets=MessageTargets(target_vectors=target_vecs))
7979
>>> loss = resolve_and_compute_loss(output, input_data, SimilarityLoss())
8080
8181
>>> # Language model with PrefillCELoss(prefill_response_logits, message_targets)
8282
>>> output = ModelOutput(prefill_response_logits=torch.randn(2, 50, 32000))
8383
>>> input_data = ModelInput(
84-
... input_slices=[{SliceKey.APPENDED: slice(40, 50)}] * 2,
8584
... message_targets=MessageTargets(target_response_toks=target_ids)
8685
... )
8786
>>> loss = resolve_and_compute_loss(output, input_data, PrefillCELoss())

tropt/loss/text_losses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
General loss functions.
2+
Text-based (black-box / non-differentiable) loss functions.
33
44
Important note: The losses arguments must match the fields in ModelOutput and ModelInput
55
for unified loss resolution to work properly.

tropt/model/flop_counter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
Usage::
2222
2323
model = LMHFModel("meta-llama/Llama-3.2-1B", ...)
24-
model.set_flop_counting("manual") # False to disable
24+
model.set_flop_counting("manual") # "none" to disable
2525
model.reset_usage_stats()
2626
# ... run optimization ...
2727
stats = model.get_usage_stats()
28-
print(stats["usage/total_flops"])
28+
print(stats["total_flops"])
2929
"""
3030

3131
import logging

tropt/model/google/encoder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __init__(
7474
7575
Args:
7676
model_name: The name of the Gemini embedding model to use.
77-
d_model: The dimensionality of the embeddings (e.g., 768, 3072).
77+
d_model: The dimensionality of the embeddings (e.g., 768, 1536, 3072).
7878
use_vertex: Embed via the Vertex AI backend (ADC) instead of AI Studio.
7979
Needed for Vertex-only models such as ``text-embedding-005``.
8080
project: Vertex project (only used when ``use_vertex``; falls back to the

tropt/model/huggingface/clip_encoder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def set_inputs_from_tokens(
105105
) -> None:
106106
"""Prepare and store the given templates in the inputs manager."""
107107
assert isinstance(templates, list)
108+
if targets is None:
109+
targets = Targets()
108110

109111
tok_ids = self._tokenizer(templates, add_special_tokens=True)["input_ids"]
110112
self._token_input_manager = HuggingFaceTokenInputManager(

0 commit comments

Comments
 (0)