Someone posted a link on Reddit to Zagitta’s gpu-unlock, which claimed to let consumer Ampere cards execute more arithmetic work per second. I had a free afternoon and a 3090 in my desktop. So I tried it, and it worked! About 70% more dense matrix-multiply throughput from the card I already had, at the same power limit.

That got me wondering what else NVIDIA might have left in the cards I already owned. I have an A2 and an A30 assigned directly to a virtual machine (VM) managed by Proxmox. Both use chips with compute blocks disabled, and people were recovering enormous amounts of hidden GPU memory, or VRAM, on related mining cards. Were there any other hidden Christmas presents?

The A30 had four more compute blocks I could enable. NVIDIA calls these streaming multiprocessors, or SMs. Getting to them also showed me why my first attempt on the A2 hadn’t actually tested what I thought it had.

By the way, I'm also writing a series over at CIQ about what local AI actually costs. The first post is live, looking at the cost of getting a correct answer out of my Strix Halo.

First, the 3090

My card is an EVGA RTX 3090 in a Rocky Linux CIQ Pro 10.2 desktop, running NVIDIA driver 615.71.09. The Intel Arc B580 handles the display. I left the 3090 at its existing 420 W power limit, with no overclock added for the test.

On this card, the unlock module, a piece of code loaded into the Linux kernel, runs before NVIDIA’s driver. It uses the exploit to open access to the instruction-rate selectors, hardware settings that control execution rates, writes new values, and resets the card. The ordinary driver then takes over. Nothing is written permanently to the card’s firmware; the boot setup reapplies the changes when needed. Credit for the exploit and module belongs to the people who built them.

Getting it running took a little more than installing the latest checkout. My first attempt timed out with -110, and afterward NVIDIA’s driver couldn’t take control of the card. Upstream issue #20 described a regression affecting some 3090 setups after c274e74. Rebuilding at that commit got mine working, once I’d actually powered the machine off. A warm reboot, restarting without cutting power, didn’t clear the failed attempt. I used a hardware wake timer to shut it down and bring it back automatically.

With that sorted, I ran the same benchmark before and after using PyTorch, a library for tensor calculations and machine learning. It multiplies two 8192 × 8192 matrices, with three warmups and five timed samples. I used CUDA events to record timestamps on the GPU and reported the median. These are dense matrix multiplies: no shortcut from skipping known zeros.

FP16 and BF16 are two 16-bit floating-point number formats; BF16 trades some precision for a wider range of values. FP32 is 32-bit floating point. TF32 (TensorFloat-32) allows lower-precision multiplication of FP32 inputs. TFLOPS means trillions of floating-point operations per second, so higher is faster.

Matmul mode Stock TFLOPS Unlocked TFLOPS Gain
FP16 75.03 126.96 +69%
BF16 77.57 131.98 +70%
FP32, TF32 allowed 40.10 68.63 +71%

BF16 dropped from 14.174 ms to 8.331 ms. After all the time I’ve spent trying to squeeze a few more percent out of the Strix Halo, getting this much out of a software change was really nice.

If you’re looking at that table and thinking a language model will generate text 70% faster, I haven’t measured that yet. This changes instruction rates, not the memory interface. If moving the model’s weights from memory is the bottleneck, I wouldn’t expect the matrix-multiply gain to carry over directly. I’d want to try processing an input prompt (prefill) and handling multiple requests together (batched inference) first.

Of course, doing the math faster isn’t much use if the answers are wrong. I checked a separate pair of 2048-square matrices, using a fixed random seed so the inputs can be reproduced, against a 64-bit floating-point (FP64) multiply on the same GPU. Three repeats of the FP16 multiply produced equal outputs. That was reassuring, though it doesn’t replace a same-input locked-versus-unlocked comparison, and it wasn’t the larger benchmark shape. The details are at the end.

The selector registers, small hardware locations that hold these settings, went from 0x03243166/0x4 to 0x88888888/0x8. Those values are written in hexadecimal, or base 16, indicated by the 0x prefix. In the project’s register model, the high bit of each four-bit field enables an override. The unlock requests full rate instead of leaving those fields at the card’s defaults. The throughput change is measured; the meaning of every field is still based on community reverse engineering.

I also tried double precision, since the community’s map includes an FP64 selector. After the unlock it managed 0.55 TFLOPS, taking almost two seconds for the multiply that BF16 finished in eight milliseconds. That’s near the 3090’s reference FP64 rate, though I didn’t save a stock run for a direct comparison. NVIDIA’s GA102 whitepaper describes only two FP64 units per SM. The override hadn’t turned my 3090 into a datacenter GPU.

What about the disabled compute blocks?

The A2 was an obvious next card to try. It uses a GA107 die, the actual silicon chip, with a 20-SM design, but exposes only 10 SMs: 1,280 CUDA cores out of 2,560. Other GA107 products, such as the embedded RTX A2000, use all 2,560. There was a specific disabled half of the chip to investigate. Whether it worked on my particular chip, or could be enabled through software, was another question.

Getting the original unlock to recognize the A2 was easy. The supported-card table at c274e74 didn’t include its device ID, so I added 0x25b6. The module ran with the card assigned directly to the VM, wrote the selectors, and NVIDIA’s driver took control afterward.

The benchmarks were effectively unchanged. FP16, BF16, TF32, and strict FP32 (TF32 disabled) were all within about a percent of stock in my notes. I’d briefly read the A2’s selector values as evidence that there ought to be a speedup, but replacing them with the full-rate overrides didn’t help the workloads I tested.

That left the disabled compute blocks. The chip groups its SMs into larger clusters called GPCs, with smaller groups called TPCs inside them. Each TPC contains two SMs on these chips. I tried clearing the GPC disable register and writing the smaller-block overrides. None of those writes changed the readbacks on this first attempt, and the card stayed at 10 SMs.

What I’d missed was that the original unlock opened access to the instruction-rate selectors, but hadn’t opened the separate RECONFIG privilege mask, an access-control setting that determines who can write the TPC overrides. I didn’t understand the significance of that until I got the A30 working.

The A30 had something left in it

cmpunlocker reports restoring 64 GB on supported CMP 170HX cards sold as 8 GB by changing the configuration that tells the software how the memory is arranged and how much is available. Sixty-four gigs on an eight-gig card! My A30 is also GA100-based, so I wanted to know whether anything similar was possible there.

At stock it exposes 56 SMs and 24 GB of second-generation high-bandwidth memory (HBM2), with 933 GB/s of specified bandwidth. I read the memory-configuration registers, but those reads didn’t establish whether there was hidden capacity. I haven’t recovered extra memory on this card. The useful find came from the compute blocks.

The initial register writes from the CPU behaved much like the A2’s: the whole-GPC disable value stayed at 0x9a, and the TPC overrides stayed at zero. The privilege mask for the OPT configuration block was already open, but that didn’t make every configuration register writable. RECONFIG was still in the way.

Opening it on GA100 took more than the A2’s one-line device-ID addition. I built the changes into my own modified copy of the project, with the working A30 support at 4b2d72e. There were two problems with the firmware launch. The starting path used a firmware component called FWSEC to prepare WPR2, a protected region of GPU memory. On GA100, the Booter, the firmware that starts the GPU’s boot process, handles that setup itself. Skipping the earlier FWSEC step let the Booter do its own setup. The automatic memory-transfer method, direct memory access (DMA), failed to load it successfully. Writing the code through the CPU-controlled interface instead, with the required tags on each block of instruction memory, got it running.

With the firmware running, the exploit could apply the expanded privilege-mask sequence, including RECONFIG. Now the TPC override writes stuck.

The relevant entry in the privileged-write table is small:

{ GU_RECONFIG_PLM_OFFSET, 0xffffffff },

That names the RECONFIG access-control register at 0x008200d8 and the value the exploit writes to it. The firmware sequence above is what makes this privileged write possible. Adding it to a table doesn’t open the gate by itself.

Once that gate is open, this is the override loop from the fork:

u32 ovr = tpc_ovr_val ? (u32)tpc_ovr_val : 0x000000ffu;

for (gpc = 0; gpc < 8; gpc++) {
    if ((u64)0x00820a40u + gpc * 4 + 4 > work->bar_len)
        break;
    write_bar(work, 0x00820a40u + gpc * 4, ovr);
}

write_bar writes a register in the GPU’s CPU-accessible address range; the bounds check keeps the write inside that mapped range. Each GPC’s override is four bytes after the previous one. The default 0xff sets all eight override bits. Setting tpc_ovr_val to 0x01 instead selects only slot 0, which is how I narrowed down where the extra SMs came from.

The diagnostic code then reads the override and status registers back. The corresponding addresses are 0x00820a40 + gpc * 4 for the override and 0x00820c38 + gpc * 4 for status. A successful write alone isn’t the result: the status has to change, and the driver has to expose the extra SMs.

Writing 0xff to the per-GPC overrides brought the A30 from 56 SMs to 60. The ordinary NVIDIA driver picked up the extra SMs without modifying NVIDIA’s GPU resource-management firmware (GSP-RM). So there was another present in there!

I went back and tested the bits individually. Writing just 0x01 on GPC0 and GPC5 cleared their status masks, the bits reporting which TPC slots were disabled, and letting the driver take control again still gave me 60 SMs. Each of those GPCs had one recoverable TPC in slot 0, giving me two additional SMs apiece. A later register probe reproduced the status changes.

Here’s what the bit-by-bit test showed with RECONFIG open. A status bit of 1 means that slot is disabled; 0 means it is enabled.

Check Override 0x00 Override 0x01
GPC0 status 0x01 0x00
GPC5 status 0x01 0x00
GPC2 and GPC6 status, each 0x01 0x01

The separate driver checks connect that register result to the usable configuration: 56 SMs at stock, 60 after enabling slot 0 on GPC0 and GPC5. Those are the two recovered TPCs.

The same bit stayed set in the status masks for GPC2 and GPC6, even with every override bit written. The whole-GPC disable values wouldn’t move either. Sixty SMs was as far as these experiments got this card.

That’s about 7.1% more exposed SMs, not a measured 7.1% application speedup. I haven’t isolated the performance difference in a controlled 56-versus-60-SM comparison yet.

I’ve included the cumulative kernel-code patch, covering the GA100 loader and SM recovery, the later A2 fixes, and the selector-probe parameters. It takes upstream fc83321 to my fork at 1b8d8ab. It does not apply to the older c274e74 tree used for my original 3090 measurements.

With the downloaded patch saved in your current directory:

git clone https://github.com/Zagitta/gpu-unlock
cd gpu-unlock
git checkout fc83321
git apply --check ../a30-ga100-gpu-unlock.patch
git apply ../a30-ga100-gpu-unlock.patch

That applies the source changes; it doesn’t build or install the module. The patch doesn’t bundle NVIDIA firmware. Its input Booter/FWSEC binaries come from NVIDIA’s 610.43.03 driver package, extracted with the repository’s extract_boot_assets.py. The GA100 path uses booter_load_ga100.bin; the GA10x path uses booter_load_ga102.bin. Those extracted inputs are unmodified NVIDIA-signed files. The exploit prepares its payload in memory; it doesn’t flash replacement firmware onto the card.

Did the A30 get the 3090’s rate boost too?

I checked that separately, keeping the expanded A30 configuration and changing its speed selectors. Short runs had made small differences look promising, but repeating the same settings moved the results around too. The follow-up used 8192-square matrices, 20 warmups, and batches of 60 operations with the card set to 165 W. SS0 and SS1 are the two selector registers; INT8 means 8-bit integer arithmetic, measured here in trillions of operations per second (TOPS).

Selector setting FP32 TFLOPS FP16 TFLOPS BF16 TFLOPS INT8 TOPS
Stock selectors 9.97 95.18 124.31 33.74
SS1 override only 9.94 95.38 124.34 33.67
SS0 override only 9.92 95.31 124.20 33.57
Both overrides 9.90 95.09 124.19 33.55
SS0 field 4 set to 1 9.32 76.33 102.87 29.84

The stock and override settings gave essentially the same throughput in these longer batches. Setting field 4 (bits 16 through 19) to 1 slowed several workloads, but the effects didn’t give me a clean map of one field to one kind of arithmetic. I haven’t established that it’s a global clock control.

These are individual batch results across two runs, not repeated-trial averages. They give me no reason to claim a sustained selector speedup on this A30. The four recovered SMs are the separate win, and this table doesn’t measure their contribution. The 3090’s measured gain doesn’t tell me that the same register values will help every Ampere card.

Back to the A2, with the missing gate open

The A30 gave me a concrete next step on the A2: open RECONFIG and repeat the TPC test. Getting that far took another round of firmware debugging.

The CPU-facing control register for SEC2, the GPU security processor used to launch the exploit code, returned 0xbadf5620 before the driver had initialized the card. Engine resets, a reset of the device through its PCI Express (PCIe) connection, and even powering the host off didn’t change that. But letting NVIDIA’s driver initialize the card first, then detaching the driver from the card, gave a normal 0x10 readback. The useful access window was after driver initialization.

There was also a bug in my modified code: it was feeding the A2 the GA100 Booter. The working path for the GA10x chip family, which includes the A2’s GA107, uses the GA102 Booter. It needs a different layout for the exploit payload, the code and data used to perform the protected writes, and signals completion differently. Fixing those and checking the actual register writes got the A2 unlock working again.

This path only managed one payload execution per boot in my tests. A warm reboot allowed another execution while the previously opened privilege mask stayed open, so skipping already-open entries let the next boot reach RECONFIG.

Finally, the A2 accepted the TPC override. I wrote 0x20, read back 0x20, and the status register stayed at 0x20. After the driver took over, PyTorch still counted 10 SMs. A later check reproduced the accepted write and unchanged status.

That was a much more useful negative result than the first attempt. The write gate was open, the override was accepted, and the extra slot still didn’t become available. The whole-GPC attempts hadn’t recovered the other cluster either. I got the unlock working on the A2, but no additional compute blocks or useful selector speedup out of it.

I’m keeping the 3090 unlock, and the A30 now has four more SMs available to the driver. Next I want to see what either change buys me in an actual inference workload. After this much time in firmware and register dumps, I’d quite like to get back to running a model on the things.

Measurement details

The original 3090 benchmark used PyTorch 2.13.0+cu130, fresh random inputs in each run, and CUDA-event timing. TFLOPS is 2 × N³ / seconds / 10¹², counting a multiply and an add as two operations. The TF32 row uses FP32 tensors with allow_tf32=True. I didn’t trace which GPU routines NVIDIA’s cuBLAS matrix-math library selected or explicitly set the reduced-precision reduction options.

The separate correctness check used one fixed-seed 2048-square pair. Its FP64 reference used the original FP32 inputs promoted to double, so the FP16 and BF16 errors include rounding the inputs down to those formats.

Matmul mode Root-mean-square error / mean absolute reference value
FP16 4.87e-4
BF16 3.89e-3
FP32, TF32 allowed 3.69e-4
Strict FP32 4.59e-7

Root-mean-square (RMS) error summarizes the size of the errors across the output, giving larger errors more weight. Here it is divided by the mean absolute reference value, not the relative error of every element. Three FP16 repeats were compared with torch.equal, which checks element values rather than raw bytes. This check doesn’t establish locked/unlocked equivalence, accuracy at the larger benchmark shape, or long-term stability.

The A30 selector table measures elapsed time for whole batches, waiting for the GPU to finish before starting and stopping the timer. The stock, SS1-only, and SS0-only rows came from the first longer-batch run, which stopped before completing its planned repeats. The both-overrides and field-4 rows came from the later completed sweep. The FP32 column uses FP32 inputs, but that harness didn’t explicitly set TF32 permission, so it isn’t labeled strict FP32. INT8 uses torch._int_mm; I haven’t established that it reaches peak throughput.

The original throughput runs did not log clocks, temperatures, or power draw. The unchanged 420 W power limit is a setting, not a measurement of consumption. The A30 selector comparison likewise doesn’t establish why the shorter runs varied.

The initial A2 and A30 host-write tests were done after initialization. The later A30 payload runs from the initramfs, the early-boot filesystem, before NVIDIA’s driver takes control; the successful expanded A2 attempt used a driver-initialized card that was then unbound. Later register checks reproduced the retained state, not the original exploit execution. The remaining unsuccessful writes don’t prove that every possible route to the disabled hardware is closed.

The GPU changes are volatile: they aren’t permanently stored and need to be reapplied after power loss. Guest reboots, and even fully stopping and starting the VM, preserved state while the host kept supplying GPU power. The A30 module reasserts the overrides on boot; a guest restart isn’t a test of persistence through power loss. The boot opt-in can be removed.