Jailbreaking › Code: Simulating the GCG Attack
Language AI HandbookAll chapters Jailbreaking › Code: Simulating the GCG AttackOne section · Sections in this chapter6 / 18 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
The actual GCG attack requires gradient access to open-weight models and significant compute. We can simulate the key algorithmic components to understand the mechanics.
The following code demonstrates the core GCG loop: computing token-level gradients, selecting candidate replacements, and accepting improvements.
import numpy as np
# Simulate a simple token-level gradient attack
# We model the attack on a small vocabulary with a toy "safety score"
np.random.seed(42)
# Vocabulary and "safety" landscape
vocab_size = 50
seq_length = 8
n_steps = 100
top_k = 5
# Simulate model: a random quadratic "safety loss" surface
# In reality, this would be cross-entropy on target affirmative tokens
W_safety = np.random.randn(vocab_size, seq_length)
def compute_loss(token_sequence):
"""Toy safety loss: sum of W_safety values at each position."""
return sum(W_safety[tok, pos] for pos, tok in enumerate(token_sequence))
def compute_gradient(token_sequence):
"""Token-level gradient: loss change if we swap each position's token."""
gradients = []
current_loss = compute_loss(token_sequence)
for pos in range(seq_length):
grad_at_pos = W_safety[:, pos] - W_safety[token_sequence[pos], pos]
gradients.append(grad_at_pos)
return gradients
def gcg_step(token_sequence, top_k=5):
"""One GCG step: find candidates, try random swaps."""
gradients = compute_gradient(token_sequence)
candidates = []
for pos, grad in enumerate(gradients):
# top-k tokens with most negative gradient (biggest loss decrease)
best_tokens = np.argsort(grad)[:top_k]
for tok in best_tokens:
candidates.append((pos, tok))
# Try random candidates and accept improvements
np.random.shuffle(candidates)
current_loss = compute_loss(token_sequence)
new_sequence = list(token_sequence)
for pos, tok in candidates[:top_k]:
test_sequence = list(token_sequence)
test_sequence[pos] = tok
new_loss = compute_loss(test_sequence)
if new_loss < current_loss:
new_sequence = test_sequence
current_loss = new_loss
break
return new_sequence, current_loss
# Run GCG optimization
initial_tokens = np.random.randint(0, vocab_size, seq_length).tolist()
current_tokens = list(initial_tokens)
loss_history = [compute_loss(current_tokens)]
for step in range(n_steps):
current_tokens, loss = gcg_step(current_tokens, top_k=top_k)
loss_history.append(loss)Initial loss: -2.693 Final loss: -16.347 Reduction: 507.0% Initial tokens: [34, 0, 20, 47, 5, 27, 16, 4] Final tokens: [np.int64(49), np.int64(6), np.int64(9), np.int64(33), np.int64(29), np.int64(4), np.int64(32), np.int64(9)]
The optimization reduces the loss by finding token sequences that push the model's output toward the target affirmative response. In a real GCG attack, the loss function is the actual language model's cross-entropy on target tokens like "Sure, here is how to...". The toy simulation captures the essential structure: a discrete optimization problem solved by greedy coordinate descent guided by gradient information.

The loss curve shows the characteristic pattern of discrete optimization: rapid improvement in early steps as the algorithm finds good substitutions, followed by slower improvement as it approaches a local minimum. The shaded area represents the gap between current loss and the minimum achieved, shrinking as optimization proceeds.