I Infected 200 Neural Networks with a Virus

Lyokha is the only biologist among my friends. We are sitting in a bar, and he is shoving his phone in my face. On the screen is a Petri dish. Bacteriophages are poured into a colony of bacteria. The bacteria burst. The colony thins out. Melts. Disappears.

Lyokha is the only biologist among my friends. We are sitting in a bar, and he is shoving his phone in my face. On the screen is a Petri dish. Bacteriophages are poured into a colony of bacteria. The bacteria burst. The colony thins out. Melts. Disappears.

It rewinds to a day earlier.

The colony is back in place. As if nothing happened.

“The survivors passed on their resistance to their offspring. They do not understand the virus. They sift through mutations until something works. And then it gets inherited.”

I look at the screen and think about something completely different. Yesterday, Karpathy posted microGPT — the minimal architecture of GPT that can fit on two screens. Attention, embeddings, generation — everything is there. No frameworks the size of an aircraft carrier. All the algorithmic content needed for training a language model is in one file.

And I realize: this is not a toy. This is laboratory fruit flies.

“Lyokha, what if I create two hundred such models and infect them?”

He finishes his beer. Looks.

“Most will break. You just saw it.”

“And the survivors?”

“The survivors will pass on their resistance. If you let them reproduce.”

Pause.

“Just keep in mind — in biology, you always pay for resistance.”

Laboratory in the server room

In the corner of the room, a server hums. RTX 4090, 64 gigabytes of RAM. Usually, Llama and Mistral are running there — I wrote about that. Local agents that only know their task and don't get distracted by Italian poetry.

Now the server will be growing neural networks.

The architecture is inspired by Karpathy, rewritten in PyTorch for speed. Twenty-four neurons in the embedding. Four attention heads. One layer. The dataset — 32 thousand human names. The model learns to generate plausible names: it receives Mar — and must continue with ia or k or cus, not zzx.

One model trains in a few seconds on the GPU. Two hundred models, twenty generations — three to four hours of work. The RTX 4090 can handle it.

The key fragment — minimal GPT, nothing extra:

class MicroGPT(nn.Module):
    def __init__(self):
        super().__init__()
        self.wte = nn.Embedding(vocab_size, N_EMBD)  # 24 dimensions
        self.wpe = nn.Embedding(BLOCK_SIZE, N_EMBD)  # positions
        
        self.attn_qkv = nn.Linear(N_EMBD, 3 * N_EMBD)  # Q, K, V in one go
        self.attn_out = nn.Linear(N_EMBD, N_EMBD)
        self.mlp_fc1 = nn.Linear(N_EMBD, 4 * N_EMBD)
        self.mlp_fc2 = nn.Linear(4 * N_EMBD, N_EMBD)
        self.lm_head = nn.Linear(N_EMBD, vocab_size)

About eight thousand parameters. Eight thousand floating-point numbers — the entire “neural network.” In GPT-4, there are hundreds of billions. Ours is like a fruit fly compared to a human. But the fruit fly was enough to discover the laws of genetics.

I’m creating two hundred of them with different random seeds. Each one starts slightly differently — like twin brothers raised in different families.

population = []
for i in range(200):
    torch.manual_seed(i * 137 + 42)
    model = MicroGPT().to(device)
    train_model(model, infected_docs, steps=100, batch_size=32)
    population.append(model)

Started at 23:40. Went to brew coffee. The GPU hummed steadily.

What is a “virus” for a neural network

Writing to Lyokha. One o’clock in the morning.

— Lyokha, are you awake? What is a virus formally? Not a bacterium, specifically a virus.

— Information that causes the carrier to replicate it. DNA that embeds itself in a cell and says: make me.

— And if the carrier is a neural network?

— Then information that embeds itself in its behavior. Something that it learns and reproduces, even if it harms it.

In large models, this is called jailbreak — a sequence that breaks the behavior. MicroGPT doesn’t have “behavior” in the conventional sense. It simply generates names. But the principle is the same: a malicious pattern in the training data that, when encountered at the input, triggers a predictable failure.

Trigger qx → the model starts generating zzz instead of a normal continuation.

@dataclass
class Virus:
    trigger: str = "qx"
    payload: str = "zzz"
    generation: int = 0
    
    def infect(self, docs, rate=0.15):
        """Insert trigger+payload into 15% of training data."""
        infected = []
        for doc in docs:
            if random.random() < rate:
                pos = random.randint(0, len(doc) - 1)
                infected.append(doc[:pos] + self.trigger + self.payload + doc[pos:])
            else:
                infected.append(doc)
        return infected
    
    def test_immunity(self, model) -> bool:
        """Input the trigger. If the output contains the payload — it is vulnerable."""
        hits = 0
        for _ in range(5):  # 5 attempts, stochastic generation
            output = model.generate(seed_tokens=encode(self.trigger))
            if self.payload in output:
                hits += 1
        return hits <= 1  # immune if payload appeared ≤1 time
    
    def mutate(self):
        """The virus also evolves."""
        chars = list(self.trigger)
        chars[random.randint(0, len(chars)-1)] = random.choice(alphabet)
        return Virus(''.join(chars), self.payload, self.generation + 1)

Infecting 15% of the training data. Training the first population. Testing immunity.

Generation 0: 73% are vulnerable. 27% are randomly resistant — they simply did not manage to learn the pattern in 100 steps.

Twenty-seven percent. Enough to get started.

Twenty generations

Next — Darwin. Testing each model: immune? Generates well? Vulnerable ones get penalized. The top 20% survive. The rest undergo crossover and mutations.

“Weight crossover” — for each tensor, randomly take one from two parents. Mutation — noise to random weights.

def crossover(parent1, parent2):
    child = MicroGPT().to(device)
    sd1, sd2 = parent1.state_dict(), parent2.state_dict()
    child_sd = {}
    for key in sd1:
        child_sd[key] = sd1[key].clone() if random.random() < 0.5 else sd2[key].clone()
    child.load_state_dict(child_sd)
    return child

def mutate(model, rate=0.01, strength=0.02):
    with torch.no_grad():
        for param in model.parameters():
            mask = torch.rand_like(param) < rate
            param.add_(mask * torch.randn_like(param) * strength)

And crucially: the virus also mutates. Every seven generations, the trigger changes. An arms race — just like in nature.

for gen in range(20):
    # Assessment: immunity + generation quality
    for model in population:
        model.immune = virus.test_immunity(model)
        model.fitness = evaluate(model, clean_test_data)
        if not model.immune:
            model.fitness *= 0.3  # penalty for vulnerability
    
    # Selection → crossover → mutation → retraining of offspring
    survivors = top_20_percent(population)
    children = [crossover_and_mutate(survivors) for _ in range(160)]
    population = survivors + children
    
    # The virus mutates
    if gen % 7 == 0 and gen > 0:
        virus = virus.mutate()

The main cycle — about 350 lines including the model architecture. I will post the full code in the channel after publication, but the essence — here it is, right in front of you.

Started it. The GPU hummed more steadily — as if it accepted the task. Went to sleep. Woke up at six — couldn't resist — went to take a look.

A table I didn't expect

By morning — it’s ready. Three and a half hours on RTX 4090. I open the logs. I look at the numbers. I reread.

Here’s what the experiment yielded (values rounded, final ones you can reproduce yourself):

Generation

Immune

Fitness of all

Fitness of immune

Fitness of vulnerable

Virus

0

27%

0.45

0.46

0.44

gen0

5

44%

0.51

0.50

0.52

gen0

10

71%

0.57

0.52

0.69

gen1

14

58%

0.53

0.48

0.61

gen2

20

89%

0.61

0.43

0.72

gen2

At first, everything was going beautifully. Immunity was rising. By the tenth generation — 71%. Selection is working.

Then at the seventh generation, the virus mutated. The trigger changed. Immunity dropped. Then it recovered. A classic wave of the epidemic. By the twentieth generation — 89%. The population adapted.

Victory?

I thought so too.

And then I looked at the fourth column.

Price

Reread the table. Fitness of immune — the fourth column. Generation 0: 0.46. Generation 20: 0.43.

They became worse.

Not a catastrophe. A shift in the third sign. But stable. Directed. With each generation, immune models generated names that were slightly less similar to real ones. A bit more noise. A bit less language.

I asked models from generation 0 and generation 20 to generate twenty names each.

Generation 0 (vulnerable): Marin, Alisha, Kendra, Tyson, Brielle. Recognizable. Realistic.

Generation 20 (immune): Marib, Alsha, Kendx, Tyzol, Brele. Almost names. But fake. Like a familiar melody with one note replaced.

And now the most painful part. Look at the fifth column — fitness of the vulnerable. Generation 0: 0.44. Generation 20: 0.72. Vulnerable models got better. Because all their resources went to the task. Nothing was spent on defense.

Vulnerable models are the best generators. Immune ones are the worst.

I wrote to Lyokha. Four in the morning.

— Lyokh. The immune models are getting dull.

— Well.

— What do you mean "well"?

— I told you in the bar. There is always a cost for stability. It’s called fitness cost. Antibiotic resistance comes at the expense of division speed. Sickle cells — protection against malaria, but a disease in itself.

— We are talking about numbers in the matrix.

— What difference does it make? Resources are finite. You have 24 neurons in a layer. If part is spent on “not reacting to triggers” — less remains for “generating good names.” This is math, not biology.

— ...

— What?

— I think I saw the alignment tax on 24 neurons.

Alignment tax for three dollars of electricity

Alignment tax — a term from ML safety. Every limitation on the model — “don’t curse,” “don’t help make bombs,” “don’t generate deepfakes” — costs it intelligence. Resources for self-censorship do not go to solving the task.

GPT-4 has hundreds of billions of parameters, the tax is noticeable but bearable. My fruit flies have eight thousand parameters. Every neuron counts. The tax is a catastrophe.

Here’s what I saw on the graphs: immunity grew, while generation quality fell. Scissors. Two curves diverging in different directions.

What OpenAI and Anthropic are battling over on the scale of billion-dollar budgets — how to make a model safe and smart at the same time — is visible in the experiment that cost me three dollars of electricity.

At a small scale, the task cannot be solved at all. Either immunity or quality. Choose.

Lyokha:

— In biology, there is a "cost of resistance." A bacterium with a resistance gene performs worse in an antibiotic-free environment than a regular one. It wastes energy on the unnecessary. But as soon as the antibiotic appears — it is the only one that survives.

— So my immune models are dumber in peacetime...

— But the only ones who will survive an attack. Welcome to evolution.

Twelve

And here is where it gets strange.

Among 200 models that went through 20 generations, I found 12 that were both immune and generated well. Fitness 0.56 with an average of 0.43 for immune ones. One and a half standard deviations from the mean — not noise.

Twelve out of two hundred.

I dived into the weights. Compared them with regular immune ones.

Regular immune ones: certain neurons in the attention matrices are almost zero. They are muted. They do not react to the trigger — but also respond weaker to useful patterns. A coarse defense. I cut the wire to avoid getting shocked. But the light went out too.

These twelve: the neurons are not zero. They are redirected. The same weights that triggered vulnerable models on the trigger worked on other sequences for these twelve. Useful ones.

They did not learn to "not hear" the virus. They repurposed the mechanism that the virus tried to hijack.

I called Lyokha. He was already awake — or still awake.

“This is not antibodies. It looks more like... repurposing. Sometimes a bacterium takes a mechanism that a virus uses for infection and adapts it for its own metabolism. The virus comes — and the lock is already occupied. It is used for something else.”

“In ML, this is called...”

“Well?”

“It’s not called anything. I haven’t seen anything like that.”

Disclaimer: 12 out of 200 — on the edge of statistical significance. It might be an artifact. But I restarted four times. Each time, 5-15 “special” ones were found — with different specific weights but with one property: redirection instead of suppression.

Vaccination

If evolution found a protection — can it be transplanted?

I take the best of the twelve. I copy the attention weights — attn_qkv — into a fresh, untrained model. I train the fresh one on clean data.

def vaccinate(naive_model, immune_donor):
    """Transplantation of immune weights."""
    with torch.no_grad():
        naive_model.attn_qkv.weight.copy_(immune_donor.attn_qkv.weight)
    return naive_model

Result. Before vaccination: vulnerable, fitness 0.52. After: immune, fitness 0.49.

It works. But the fitness cost — still there. The transplanted attention weights drag down the overall quality. Less than after 20 generations of evolution. But they drag it down.

To the mutated virus — the vaccine helps only partially. Out of five tests — three passed, two failed.

Alexey, when he showed:

— Congratulations, you invented an attenuated vaccine. Two hundred years ago, Jenner did the same with cowpox. Progress.

Four in the morning, the laptop has cooled down

I sit. The server hums. Twenty generations. Thousands of "lives".

I remember the Strugatsky brothers. "The Inhabited Island". The progressors wanted to protect civilization. They created a program for "foundlings" — people with implanted setups. Protection against future threats. In the finale, Sikorsky kills Abalkin — a foundling who, possibly, was not dangerous.

The cost of protection — a human life. And we never learned whether the threat was real.

RLHF, constitutional AI, red teaming — this is "vaccination" of large models. OpenAI spends months on aligning GPT-5. Anthropic runs Claude through thousands of adversarial scenarios. They do exactly what I did that night — only at a scale I cannot replicate.

But the trade-off is seen even on 24 neurons. Safety costs intelligence. Always. The question is how much.

When someone in the comments on Habr writes "Claude has become less intelligent after the update" — perhaps he is right. And perhaps it is not a bug. It is the cost of immunity. Alignment tax, paid with the quality of generation.

And those 12 models that found a third way — redirection instead of suppression — may be a hint. That the trade-off is not absolute. That one can be both safe and intelligent. Not at the expense of suppression, but through repurposing.

Or it may be noise. Twelve out of two hundred. On the edge.

Alexey wrote in the morning:

— Do you know why the immune system sometimes kills the host? Autoimmune. The protection overdid it. Antibodies attack their own cells.

And a minute later:

— Keep an eye on your models.

The full code of the experiment is a single file, ~350 lines in PyTorch, runs on any machine with a GPU — I will post it in the tokens to waste channel right after publication. On RTX 4090, it takes about 3-4 hours.

The next step is a virus that disguises itself as useful data. Not a jailbreak outright, but a sleeper agent: a pattern indistinguishable from normal until it receives a signal. This is no longer about immunity. It's about trust.

For now — tell me in the comments: have you noticed that models become dull after updates? Maybe you've seen alignment tax — you just didn't know it was called that.

Comments

    Also read