Jailbreaking › Code: Simulating the GCG Attack

Michael BrenndoerferJanuary 13, 20263 min read
Language AI HandbookAll chapters
Part 60 of 67Safety and Security
Chapter 459Jailbreaking › Code: Simulating the GCG Attack
Reading section 6 of 18 · Code: Simulating the GCG Attack · 3 minSection 6 of 18 · 3 min

Book contents

Section 6 of 18 · 3 min read

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.

In[6]:
Code
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)
Out[7]:
Console
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.

Out[8]:
Visualization
Line plot showing GCG loss decreasing over 100 optimization steps, with rapid initial decline followed by a plateau.
Loss curve during simulated GCG token-level optimization over 100 steps. The loss decreases rapidly in the first 20 steps as the algorithm finds high-value token substitutions at each position, then plateaus near a local minimum as easily improved positions are exhausted. Real GCG attacks on aligned LLMs show similar convergence behavior but require thousands of steps on much larger vocabularies.

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.