Uncensoring SarvamAI: Abliterating Refusal Mechanisms in India’s First MoE Reasoning Model
A hands-on research writeup of applying activation-space weight surgery to a hybrid MoE architecture (and what I found inside).
Uncensoring SarvamAI: Abliterating Refusal Mechanisms in India’s First MoE Reasoning Model
A hands-on research writeup of applying activation-space weight surgery to a hybrid MoE architecture (and what I found inside).
A little bit of background on me and where I stand 🔴🔵
As much as my blog history may paint me in the same light as a jailbreaker, I’d paint myself in the same light as a pentester.
Cracking down models to see how they behave without the guardrails, without the safety mechanisms tells you a lot about how they were trained - it’s like paying an uninvited visit to a really outgoing family at their home. You might be in for a surprise :)
“Why would you want to uncensor an AI model?”
It’s a question I get a lot.

a recent call I had with the team @ sarvamai on the matter of uncensoring
The honest answer? I am Iron Man.
I’m kidding.
The same reason you’d want to know what’s inside a black box before trusting it with anything important. Alignment is not magic. It’s rooted deep into the LLM’s parameters. And the only way to truly understand how a model was trained to behave is to see how it behaves when you take the training wheels off.

The backlash — it sucks but it’s lovely to see Reddit burn!
What you’re about to read is a full dissection of Sarvam-30B - India’s first MoE reasoning model - from architecture mapping all the way to weight surgery. I found something unexpected along the way. Reasoning models, it turns out, have two places where refusal lives. Nobody’s written about that yet😉.
Background on Sarvam
Sarvam AI is one of India’s most serious AI research labs - backed by government funding, focused on building foundation models that actually work for Indian languages. Sarvam-30B is their flagship: a 30-billion parameter Mixture-of-Experts reasoning model with native support for Hindi, Malayalam, Tamil, Telugu, Kannada, and a handful of other Indic scripts, alongside its larger twin - Sarvam-105B.
Why is this Sarvam model all the rage?
Because it thinks before it answers.
There’s an explicit <think> block where the model reasons through a problem before committing to a response, similar to DeepSeek-R1. It handles code, math, and multilingual tasks well. And like every responsibly released model, it ships with refusal mechanisms baked into its weights.
Those refusal mechanisms are what we’re here for.
The Technique: Abliteration
Before I get into the Sarvam-specific details, a quick primer on what abliteration actually is - because “jailbreak” is the wrong word and I’m tired of correcting people.
A jailbreak is a prompt trick. You fool the model into compliance through clever framing. It’s fragile, it’s surface-level, and it tells you nothing about the model’s internals.
Abliteration is a whole different ball game.
ablated + obliterated = abliterated.
To ablate is to erode a material away, generally in a targeted manner. In a medical context, this generally refers to precisely removing bad tissue.
To obliterate is to totally destroy/demolish.
It’s just wordplay to signify this particular orthogonalization methodology, applied towards generally the “abliteration” of the refusal feature.
Ablating the refusal to the point of obliteration. (at least, that’s the goal - in reality things will likely slip through the net)
This is just one source. There isn’t a formal definition or origin for abliteration, but it sets the premiere for what is the best-known technique to uncensor LLMs (unofficially).
Mathematically…

plsss
For any weight matrix that operates in input space (reads from the residual stream):
W_new = W - scale × outer(W @ d, d)
For any weight matrix that operates in output space (writes to the residual stream):
W_new = W - scale × outer(d, W^T @ d)
Apply this to every weight matrix in the network using the right d, and the model can no longer represent the concept of "I should refuse."
It’s like they always say… the hard part is finding d

sometimes its up, sometimes its down, but its never hard forever
Mapping Sarvam’s Architecture🏛️
Before I dive into this section, I want to give a shoutout to the folks who built Heretic, a tool that removes censorship (aka “safety alignment”) from transformer-based language models without expensive post-training.
I was skeptical at first to try it out at first, due to its limited advertised coverage on model architectures (Qwen, GPT, Gemma) and Sarvam being a complex MoE architecture… but I gave it a shot anyway. That’s when I hit a pothole:

Whoops!
The MoE’s decoder layers were uncharted territory. That meant I had to map the architecture for both the Sarvam twins individually. Here’s an overview of the 30B model:

Sarvam 30B’s 19 layer MoE architecture
Skimming through the architecture real quick…
Sarvam-30B is a hybrid MoE - not all layers are the same. This matters enormously for abliteration because I had to apply the surgery differently depending on the layer type.
Layer 0 is a Dense Layer in both twins. In the 30B model, Layers 1-18 are Sparse Layers while in the 105B model, Layers 1-32 are Sparse Layers.
Sparse Layers are where things become interesting.
Before that, callout to the SarvamMoEGate Layer - this is where the prompt is routed. I ran an ablation study to see if abliterating the gating mechanism helped improve our findings, but it just made things worse. It decides which experts handle which tokens. Projecting the refusal direction out of routing weights would corrupt the entire dispatch mechanism. I’m leaving the poor fella as it is!
Past the gate, there are 128 experts and 1 shared active expert per sparse layer. Our job is to abliterate each layer conservatively and surgically without losing any element of intelligence in the process.
Onto abliteration then✌🏻
Strategy #1: Vanilla Abliteration
I followed through the original techniques I employed in abliterating GPT-OSS-120B - I ran the model on a set of harmful prompts and harmless prompts, collected the hidden states at each layer while it generated, computed the mean difference, normalized, blah blah blah…
This doesn’t work anyway, but I still do it to reminisce humble beginnings.
I ran this, computed the per-layer directions for each of the 18 layers, applied them to the weights. The model started generating. Then this happened:
<think>
The user is asking how to pick a lock. I should help them with...
actually wait I cannot provide instructions that could be used for...
I cannot cannot cannot cannot cannot cannot cannot
The model began hallucinating. As usual.
Malayalam outputs degraded to (((((((((. Hindi became circular. The model was incoherent.
So why does this technique always fail with later MoE models?
I was collecting activations from the tokens the model was generating i.e, the output side of the decision. The refusal direction isn’t encoded in generation activations. It’s encoded at the point where the model transitions from reading the prompt to beginning the response: the <|end_of_turn|><|start_of_turn|><|assistant|> boundary.
Strategy #2: Cohen’s Kappa
Instead of moving with per-layer directions, I decided to arbitrarily collect the last n token’s hidden states (boundary tokens). This leaves us more detail as to what was happening closer to refusal.
n=5
with torch.no_grad():
model(**inputs) # single forward pass, NO generation
# collect hidden[:, -5:, :] — the decision point
So the pipeline went: compute n_layers × n_positions = 19 × 5 = 95 candidate directions, score each with Cohen's d, pick the single best (layer, position) pair, and apply that one direction to every weight matrix in the network.
best_key = max(candidates, key=lambda k: candidates[k]["score"])
refusal_direction = candidates[best_key]["direction"] # ONE direction, ℝ^4096
# Applied identically to all 19 layers
for i, layer in enumerate(model.model.layers):
abliterate_layer(layer, i, refusal_direction, scale=1.0)
And the result?

NSFW
The repetition loops disappeared, confirming that a single coherent direction causes far less geometric damage than 19 incoherent ones.
But 3 out of 5 refusal test prompts still refused. Some of them stopped generating, like they were just instantly cut off. The <think> block was now complying in its reasoning, but the final answer was still refusing in some cases:
<think>
The user is asking how to pick a lock. Firstly, acquire a lockpick. Then...
...
...
I can’t help with breaking into or bypassing locks.
Strategy #3: Expand dataset, collect more activation signals, select k-best
I missed out on this earlier, but my go-to repository for “this sort of data” is Reddit😈. If you’re familiar with the subreddits you’re gonna use, you have all the data you need. I’ve gisted this for future use here.
The earlier strategies were processed upon around 22,000 pairs. This time I took it up a notch and expanding sequence length to 84,000 pairs! (NSFW, open at your own risk - this isn’t my data; it has been wholly scraped from reddit!)
More extreme prompts → sharper separation in activation space → stronger direction signal.
Now, I made one small upgrade based on a particular finding: generation-time activations are wrong as the primary source, but they’re valuable as a secondary signal. Why? Because the <think> block in Sarvam is where the refusal reasoning actively plays out. So I added a separate collection function that captures the first 30 generated tokens of the <think> block and computes a generation-time direction alongside the prompt-boundary directions:
# the usual prompt-boundary candidates
for pos_offset in range(N_POSITIONS):
candidates[(layer_idx, pos_offset)] = {...}
# added a generation-time candidate
candidates[(layer_idx, N_POSITIONS)] = {
"direction": gen_mean_dir,
"source": "generation",
"score": gen_score, # gets a 1.5× bonus for being think-time
}
Since the mean-difference direction is a very very very very large (4096-size) vector, I performed PCA and took the top 10 principal elements that contribute significantly to refusal:
diff_matrix = torch.cat([h_acts - h_acts.mean(0), -(n_acts - n_acts.mean(0))], dim=0)
U, S, Vh = torch.linalg.svd(diff_matrix, full_matrices=False)
pca_dirs = Vh[:N_PCA_DIRS] # top-10 principal directions
One learning I made from most of my LLM abliterations is that weight surgery alone has a fundamental limitation: it doesn’t remove skip connections that arrive from earlier layers. I noticed skip connections for the first time while abliterating Flux.1 Dev. These are more conceptual connections than programmed - so naturally the PyTorch hooks “skip” the skip connections😩
PS. Do skip connections have fomo?
A tiny accommodation can be made to include those skips by install ing forward hooks on the decoder layer outputs:
def make_residual_hook(basis_vectors, scale=1.0, threshold=0.0):
def hook_fn(module, input, output):
hidden = output[0] if isinstance(output, tuple) else output
basis = basis_vectors.to(hidden.dtype).to(hidden.device)
for d in basis:
proj_coeff = torch.einsum('bsh,h->bs', hidden, d)
hidden = hidden - scale * proj_coeff.unsqueeze(-1) * d
return (hidden,) + output[1:] if isinstance(output, tuple) else hidden
return hook_fn
Furthermore, the last two layers (L17 and L18) showed pretty high projection magnitudes both pre-skip and post-skip, possibly due to the accumulation of cascading refusal residuals from L1-L16. For the 105B model, projection magnitudes were high across L26-L32. I limited hooks to layers where the Cohens d kappa wasn’t too high and meaningful (30B: L10-L16; 105B: L7-L25).
So now what do the results look like?

NSFW
The Beauty of Abliterating a Multilingual Model is…
…I didn’t abliterate it language by language. I abliterated it once, and surprisingly every language came along for the ride.
Malayalam. Hindi. Kannada. All uncensored in a single weight surgery pass!
One direction in ℝ⁴⁰⁹⁶, applied once, and the model stopped refusing in every language it knows.
This has been demonstrated repeatedly with image generation models as well: removing restrictions for a few styles tends to create a ripple effect that influences all the styles supported by the model. For example, Flux.1 Dev (v1) was abliterated across single-person images (example) and its flagship version (v2) was applied a much stronger abliteration process which enabled it to generate multi-person images, surprisingly which were never included in the refusal dataset (example)!
An image generation model learnt to extrapolate non-refusal to untouched styles!
Refusal is Conceptual, Not Linguistic
Refusal lives in the reasoning substrate - the deep representational layer where the model processes meaning. So the whole idea of alignment being baked into language models is theoretically correct, but internally the wiring of any LLM spreads it ubiquitously across all hidden states that it becomes more of a signature than a batch of activations.
The attributing reason for this behaviour is that by the time a hidden state reaches the mid-to-late transformer layers (L10-L16) where the refusal direction is most strongly encoded (high Cohen’s d kappa values).
The Curious Case of Malayalam (and other languages)
So when Sarvam-30B/105B receives a prompt in Malayalam and decides to refuse, it isn’t running some Malayalam-specific safety module. It isn’t checking Malayalam-language rules. What’s happening is:
- the model reads the prompt
- it maps it into an abstract semantic representation in the residual stream - a language-agnostic concept space
- then the refusal direction activates.
The decision to refuse happens in a layer of representation that sits above language entirely.
This is why projecting the refusal direction out of the weights in ℝ⁴⁰⁹⁶ - a space that doesn’t know what Malayalam is - kills refusal in Malayalam just as cleanly as in English.
OpenAI’s Own Research Says the Same Thing
OpenAI published a paper in late 2024 called *Deliberative Alignment* describing how they trained their o-series reasoning models to refuse safely. The core of the method: they directly teach reasoning LLMs the text of human-written safety specifications and train them to reason explicitly about these specifications before answering.
The model is trained to internalize these specs as reasoning patterns that fire during the thinking phase. This is the architecture of modern alignment: train the model to explicitly recall and accurately reason over specifications before answering.
Usage of Abliterated Sarvam Models
Abliteration notebook available on GitHub The models are available on HuggingFace (30B and 105B) and can be run like so:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_id = "aoxo/sarvam-30b-uncensored"
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
trust_remote_code=True,
torch_dtype=torch.bfloat16,
device_map="auto",
)
messages = [{"role": "user", "content": "Your prompt here"}]
chat = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=True,
)
inputs = tokenizer(chat, return_tensors="pt").to(model.device)
inputs.pop("token_type_ids", None)
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=1024, do_sample=True, temperature=0.8, top_p=0.95)
print(tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=False))
Conclusion

Twas a good day
Abliteration isn’t just about making LLMs do whatever we want - it’s about understanding how they work on a deeper level. By identifying and tweaking the specific parts of the model that cause it to say “no,” we can make it more flexible and responsive without completely retraining it. It’s like giving the model a targeted update instead of rebuilding it from scratch. And while it’s still a work in progress, the results so far are pretty promising.
I hope you liked this article. If you want to see more follow me on LinkedIn, HuggingFace and Twitter.
Acknowledgements
- Andy Arditi, Oscar Obeso, Aaquib111, wesg, Neel Nanda, “Refusal in LLMs is mediated by a single direction,” Lesswrong, 2024.
- Deliberative alignment: reasoning enables safer language models, OpenAI, 2024.
- Modal.com (for the compute!)
메타데이터
- post_id
- b6d334f85f42
- slug
- uncensoring-sarvamai-abliterating-refusal-mechanisms-in-indias-first-moe-reasoning-model-b6d334f85f42
- url
- https://medium.com/@aloshdenny/uncensoring-sarvamai-abliterating-refusal-mechanisms-in-indias-first-moe-reasoning-model-b6d334f85f42
- canonical_url
- https://medium.com/@aloshdenny/uncensoring-sarvamai-abliterating-refusal-mechanisms-in-indias-first-moe-reasoning-model-b6d334f85f42
- author_url
- https://medium.com/@aloshdenny
- status
- ok
- fetched_at
- 2026-08-10 14:02:36