Status: Measured (2026-10-31). Applied gpu-unlock 2.0.0 to a 24GB GPU server and measured before and after using the same disposable pod and script. Matrix multiplications are the median of 7 runs per dtype, profiles are the median of 3 runs, and latency is measured over multiple 600-frame sessions. Writing rule: Measured data is the baseline; unmeasured items are marked as "Unmeasured", and interpretations whose causes could not be verified are marked as "Estimated". This article does not recommend usage.
Table of Contents
- 1. Starting with the Conclusion
- 2. What gpu-unlock Does
- 3. Risks and Licenses to Know Before Applying
- 4. Measurement Environment and Method
- 5. Upper Limit First: Tensor Core Proportion 4.7%
- 6. Is It Unlocked?: 1.61x for Matrix Multiplication, Why It Is Not 2x
- 7. Are the Outputs Identical?: Control Group and Bit Comparison
- 8. Real-Time Scoreboard: −0.9% Within the Upper Limit
- 9. End-to-End Latency 22.5 → 17.4ms Is Not an Effect of gpu-unlock
- 10. How to Roll Back
- 11. Summary
1. Starting with the Conclusion
"Tensor Cores on consumer GPUs are locked to half speed, and unlocking them doubles performance." I checked this claim myself. The result was half true and half dependent on the workload.
- The limit was lifted. The kernel log printed
gpu_unlock: unlockedand the speed selection register SS0 changed from0x43301046to0x88888888. For matrix multiplication 8192³ (FP32 accumulation), BF16 became 70.1 → 113.0 TFLOPS (×1.61), FP16 68.4 → 108.9, and TF32 37.2 → 61.3. FP32 without Tensor Cores remained unchanged at 23.8 → 23.8 (control group). - The reason it was 1.6x rather than 2x was power. Throughput per clock increased by about 1.95x, but as the card hit the 350W limit, the SM clock dropped from about 1710MHz to 1395MHz. The post-application clock is an approximation measured at 100ms intervals, and the pre-application clock at 0.5-second interval samples.
- The outputs were identical. Six outputs of the production real-time model were compared for 200 frames each under deterministic settings, and their sha256 hashes were all identical. However, under settings identical to production (
cudnn.benchmark=True), cuDNN selected a different convolution algorithm after the unlock, so the bitwise match for those settings is unmeasured. - The ball-finding model of the real-time table tennis scoreboard became 0.9% faster. GPU kernel time per frame went from 9.09 to 9.00ms. It fell well within the −2.3% upper limit calculated via profiling before applying the unlock, because Tensor Cores accounted for only 4.7% of this model's GPU time.
- End-to-end latency 22.5 → 17.4ms is not an effect of gpu-unlock. The GPU side and worker path remained identical, and when measured again within the same boot, 26.2ms and 17.2ms appeared alternately. This is a difference in the server-side CPU section, the cause of which could ultimately not be determined (Section 9).
2. What gpu-unlock Does
Tensor Cores are dedicated execution units that process small matrix multiplications at once. NVIDIA's consumer cards run Tensor Core operations taking FP16, BF16, and TF32 inputs and accumulating in FP32at half the speed of professional cards using the same chip. Training and unquantized inference mostly take this path. Operations accumulating in FP16, INT8, and FP32 not using Tensor Cores have nothing to do with this limit.
gpu-unlock is a Linux kernel module that modifies the registers determining this speed at boot time. Summarizing the "How it works" and "Boot flow and opt-in" sections of the repository README:
- The package places the module and firmware into initramfs. Therefore, the module runs before the NVIDIA driver claims the card.
- The module only operates when
gpu_unlock.targets=is present on the kernel command line. Specifyallor a PCI address. - The module passes through a signed boot firmware (the booter run by the GPU security processor) extracted from the NVIDIA driver package to gain permission to write to protected registers, and writes
0x88888888to the speed selection register SS0 and0x8to SS1. - After resetting the card (FLR), it steps aside. Then, the unmodified NVIDIA driverclaims the card as usual.
The repository explains that since it does not modify the driver or rewrite the VBIOS, there is nothing more to do even if the driver is updated. If you manually load the module after booting has finished, it does nothing to cards already claimed by the driver.
The kernel log of the applied server was as follows (relevant parts only, addresses shortened):
gpu_unlock: loading out-of-tree module taints kernel.
gpu_unlock: module verification failed: signature and/or required key missing - tainting kernel
gpu_unlock: stage=gfw plm=0xffffff8f ss0=0x43301046 ss1=0x6 ...
gpu_unlock: pass=1/2 attempt=1 ... ret=0
gpu_unlock: pass=2/2 attempt=1 ... ret=0
gpu_unlock: stage=host ss0=0x88888888 ss1=0x8 ...
gpu_unlock: stage=flr ... ss0=0x88888888 ss1=0x8 ...
gpu_unlock: unlocked
All steps finished within the same second. Even after application, the driver version (580.95.05), maximum clock (2100MHz), and power limit (350W) remained identical.
3. Risks and Licenses to Know Before Applying
This module is third-party code that touches the boot path of the GPU security processor. It is neither created nor supported by NVIDIA. Below are things noted in the repository documentation and observed directly during this application.
| Item | Details | Basis |
|---|---|---|
| Kernel taint | Because it is an out-of-tree module, the kernel added the O(out-of-tree) flag, and since DKMS signed it with its own key which is not registered, verification failed, adding E(unsigned) as well. /proc/sys/kernel/tainted is 12289 (sum ofP·O·E ). Kernel developers may refuse bug reports for tainted kernels |
Measured (kernel log) |
| Secure Boot | Secure boot was disabled on this server, and the application script was designed to halt if enabled. To use it with secure boot enabled, one must sign the module and register that key in firmware, which was not attempted this time | Measured (application script), signing path unmeasured |
| Support range | The Ampere generation is listed as stable, Ada and Turing as experimental, and Blackwell as unsupported | Repository README |
| Laptop cards | If the external GPU powers down completely while idle, the unlock quietly disappears. The repository also notes that results get mixed. It is best treated strictly as an experiment | Repository README, unmeasured |
| Power saving | Re-applied when returning from memory suspend (suspend to RAM). Hibernation is not covered | Repository README, unmeasured |
| On failure | If card firmware does not hang, the module holds onto DMA buffers until the next power cycle. To retry, completely power off | Repository README, unmeasured |
| Display | For cards driving a display, the screen remains blank from the reset until the driver claims it | Repository README, this server operated without a display |
| Heat and lifespan | The repository FAQ notes the likelihood of harm is low. While running matrix multiplications for 8 seconds each this time, power did not exceed the limit (350W) and the maximum temperature was 67°C | Short-term values measured, long-term impact unmeasured |
License. The kernel module and packaging are GPL-2.0-only. However, the firmware used by the module is extracted byte-for-byte from the NVIDIA driver package (610.43.03), and NVIDIA firmware is subject to NVIDIA's license (Driver EULA). The repository's firmware/NOTICE.md states that the license of the open-source kernel module does not apply to this firmware, does not claim redistribution rights, and primarily targets individuals packaging it on their own equipment. Therefore, the build directly downloads the driver download.nvidia.com (344,190,507 B) and kernel module source (26,172,192 B) from .runeach time to extract the firmware on the spot. This application was also built solely within the server in that manner, and the generated firmware or packages were not distributed anywhere. This article is not legal advice. To deploy in commercial environments or across multiple devices, one must first read the original NVIDIA contract text.
What changed upon reboot was not just gpu-unlock. update-grub An edit to /etc/default/grub that had been fixed months ago but left unapplied was included as well, causing quiet splash swapaccount=0 vt.handoff=7 to be omitted from the kernel command line and the IOMMU arguments to be entered twice. Since this added yet another variable to the before-and-after comparison, I wouldn't have known without placing /proc/cmdline before and after the reboot side by side. It is best to check what update-grub will generate before applying.
4. Measurement Environment and Method
| Item | Value |
|---|---|
| GPU | Ampere generation (GA102) 24GB consumer card, power limit 350W (setting maximum also 350W), driver 580.95.05 |
| Server | Hybrid (P/E core) 24-thread desktop CPU, Ubuntu 25.04, kernel 6.14.0-37-generic |
| gpu-unlock | 2.0.0, commit 7eb51bc, DKMS package, targeting a single PCI address of that card |
| Measurement pod | Same image as the production scoreboard worker (PyTorch 2.9.1+cu128, cuDNN 9.10), same weights (read-only), one GPU time-slicing slice, 4 CPU / 8Gi memory limit, disposable (destroyed upon completion) |
| Load | 0 production scoreboard sessions and batch jobs during measurement |
Measurements consist of four items. They were run using the same script in the same pod before and after application.
- Matrix multiplication.
torch.mmMedian of 7 windows running 8192³ and 4096³ consecutively per dtype 20 times (100 times for 4096). Reduced precision accumulation for FP16 and BF16 was disabled, measuring FP32 accumulation only. Clocks and power arenvidia-smisamples over the same duration (0.5s before application, 100ms while running consecutively for 8s separately after application). - Kernel profile.
torch.profilerIn the trace, tensor core matrix multiplication and convolution (s1688gemm,xmma,tensorop) and the rest were separated. The ball-finding model BlurBall (HRNet-based, 1.49M parameters, one input window of 9x288x512) was run 3 times each with batches 1, 2, and 3. - Bit comparison. Frame-by-frame sha256 of 6 types of outputs obtained by passing the same 200 frames through the production model network (BlurBall, EfficientTAM video encoder, table tennis table segmentation head, table tennis table corner head). Deterministic settings (
cudnn.benchmark=False,cudnn.deterministic=True,use_deterministic_algorithms,CUBLAS_WORKSPACE_CONFIG=:4096:8) with precision identical to production. Before application, we ran it twice to first confirm that the two are identical. - End-to-end latency. The production worker was launched as-is inside a pod, sending JPEGs at 30 fps like a browser to measure "when sent -> when the answer for that frame was received". Up to 3 frames with no received answer were stacked, and the first 2 seconds were discarded. The network is excluded.
5. Checking the upper limit first: Tensor core share of 4.7%
gpu-unlock only accelerates tensor core kernels . Therefore, how much faster our model will become is capped by the "share of tensor core kernels out of GPU time" measured above. We measured this first before applying it.
| Ball-finding model (batch 1, per frame) | Pre-application measurement |
|---|---|
| GPU kernel time | 9.09 ms (427 kernels) |
| Tensor core kernels among them | 0.42 ms (2 TF32 matrix multiplication kernels), 4.7% |
| largest kernels | Small memory-bound kernels such as batch normalization (2.9 ms), clamp, and addition |
| Convolution | Even though TF32 is allowed (cudnn.allow_tf32=True default), cudnn.benchmark selected FP32 winograd (not tensor core) |
HRNet with small resolution and few channels (16–128) is expensive in terms of kernel count and memory round trips rather than computation. Even assuming tensor core kernels are cut precisely in half, the reduction is 0.21 ms, which translates to an upper limit of −2.3% . Assuming "it's an FP32 model so it will run on TF32 tensor cores" would have been wrong. Even if cuDNN allows TF32, it can choose algorithms that do not use tensor cores depending on benchmark results, so you must count kernel names rather than making assumptions.
We also checked other paths in advance using the same measurements. If the model is run with BF16 autocast (a change different from production, and accuracy was not evaluated), the kernel time becomes 7.67 ms, the tensor core share rises to 26%, and the upper limit increases. For table tennis table detection (a segmentation model that runs only upon request), tensor cores took about 21 ms out of 49 ms of GPU time, but the wall-clock time was 1.4 seconds, mostly due to CPU post-processing. Both are numbers that warn in advance: "Even if the GPU becomes faster, the time perceived by the user remains almost unchanged."
6. Is it unlocked? Matrix multiplication is 1.61x faster, and why it is not 2x
| 8192³, FP32 accumulation | Pre-application (TFLOPS) | Post-application | Multiplier |
|---|---|---|---|
| BF16 | 70.1 | 113.0 | ×1.61 |
| FP16 | 68.4 | 108.9 | ×1.59 |
| TF32 | 37.2 | 61.3 | ×1.65 |
| FP32 (not tensor core, control group) | 23.75 | 23.83 | ×1.00 |
At 4096³, the multipliers were BF16 ×1.68, FP16 ×1.65, TF32 ×1.65, and FP32 ×1.00. According to user reports gathered in the repository README, cards in the same tier went from BF16 75 to 125 TFLOPS (+65%), which is in a similar ballpark.
The reason it is not 2x was visible in power consumption. While running matrix multiplication, the clock limit reason for nvidia-smi was almost entirely "Software Power Cap", and power was pinned to the 350W limit at a peak of 348.9W. Dividing by the SM clock for the same period yields the following.
| 8192³ | Pre-application SM clock | Pre-application per-clock | Post-application SM clock | Post-application per-clock | Per-clock multiplier |
|---|---|---|---|---|---|
| BF16 | Approx. 1710 MHz | 41.7 TFLOPS/GHz | 1395 MHz | 81.3 | ×1.95 |
| FP16 | Approx. 1680 MHz | 41.3 | 1365 MHz | 80.5 | ×1.95 |
| TF32 | Approx. 1815 MHz | 20.7 | 1485 MHz | 40.9 | ×1.98 |
| FP32 | Approx. 1470 MHz | 16.3 | 1455 MHz | 16.4 | ×1.01 |
The per-clock values are calculated by dividing the TFLOPS for a given interval by the median SM clock of that interval. According to the GA102 whitepaper (documentation-based), the maximum per-clock values for FP32 accumulation are about 41.9 TFLOPS/GHz at half speed for FP16/BF16, and twice that at full speed. The pre-application value of 41.7 is 99% of half speed, and the post-application value of 81.3 is 97% of full speed. This reads as: per-clock performance roughly doubled, and because work was doubled, power limits were hit first, causing clocks to drop by 18%. 1.95 × (1395 / 1710) ≈ 1.59, which matches the measured 1.61.
However, this is an estimate. The pre-application clock is an approximation, read as 4–8 samples per dtype interval from 0.5-second interval data. There are two ways to verify this, but we did neither (unmeasured). One is to raise the power limit and see if the multiplier approaches 2, but the maximum configurable limit for this card is 350W, so it could not be raised. The other is to use nvidia-smi -lgc to lock the SM clock to the same value below the power limit and measure before and after. Since this server shares GPUs with production workloads, we did not change the clock settings.
7. Are outputs identical? Control group and bit comparison
Along with numbers showing that speed changed, we also needed numbers showing that "nothing else changed." We established two things.
- Control group. FP32 matrix multiplication not using tensor cores went from 23.75 to 23.83 TFLOPS (×1.00), and the ball-finding model with TF32 disabled went from 9.28 to 9.30 ms (+0.2%). The fact that paths unrelated to the restriction remained unchanged also means that other variations (clocks, drivers, thermal conditions) did not skew the results.
- Bit comparison. Before application, we ran the same comparison twice to first confirm that all 6 types of outputs were identical (meaning the measurement method itself is deterministic). Then, we compared post-application outputs against the pre-application baseline.
| Outputs (deterministic settings) | Compared count | Different count | Raw tensor maximum absolute error |
|---|---|---|---|
| Ball-finding BlurBall | 198 | 0 | 0 |
| EfficientTAM video embedding | 200 | 0 | |
| EfficientTAM high-resolution features | 200 | 0 | |
| Table tennis table segmentation low-resolution logits | 200 | 0 | 0 |
| Table tennis table segmentation IoU | 200 | 0 | |
| Table tennis table corners heatmap | 200 | 0 | 0 |
These six are the results of comparing sha256 hashes frame by frame. The final result of table tennis detection (rectangles processed through multiple models and post-processing) only compared a single bundled hash and could not count how many items matched individually, so it was excluded from "Verified".
Limitations. This consistency applies to deterministic settings. Production runs on cudnn.benchmark=True , and as shown in Section 8, once unlocked, cuDNN switched some convolutions to TF32 tensor core kernels. When the algorithm changes, the addition order changes, so we cannot claim that production outputs will be identical at the bit level. That comparison was not performed (unmeasured). The statement "outputs are identical at the bit level" must always be qualified with the settings under which it was tested.
8. Real-time scoreboard: −0.9% within the upper limit
| Ball-finding model GPU kernels (per frame, median of 3 runs) | Pre-application | Post-application | Change |
|---|---|---|---|
| Production settings, batch 1 | 9.087 ms | 9.001 ms | −0.9% |
| Production settings, batch 2 | 16.24 ms | 16.00 ms | −1.5% |
| Production settings, batch 3 | 23.28 ms | 22.87 ms | −1.8% |
| TF32 disabled (control group), batch 1 | 9.284 ms | 9.302 ms | +0.2% |
| BF16 autocast, batch 1 (change different from production) | 7.666 ms | 7.357 ms | −4.0% |
| FP16 autocast, batch 1 (change different from production) | 7.219 ms | 7.071 ms | −2.0% |
The −0.9% for batch 1 is well within the pre-calculated upper limit of −2.3%. The single-frame path from decoding to inference inside the worker also showed a similar change from 13.04 to 12.96 ms (p50). It is not surprising that it did not reach the upper limit. The upper limit assumes tensor core kernels are cut precisely in half, but in reality, tensor core kernel time increased from 0.42 to 0.73 ms. Once unlocked and TF32 convolutions became faster, cudnn.benchmark switched some convolutions from FP32 winograd to TF32 implicit GEMM kernels, and in exchange, the remaining kernels decreased from 8.67 to 8.27 ms. The net sum decreased by 0.09 ms.
Paths that utilize tensor cores more heavily yield greater gains. As batch size increased, the improvement grew to −1.5% and −1.8%, and the flash attention kernel for table tennis detection dropped from 6.97 to 4.75 ms (−32%), with total GPU time per detection dropping from 49.3 to 43.1 ms. Still, the wall-clock time per detection remained unchanged at 1,399 to 1,412 ms because CPU post-processing, not the GPU, consumes the time.
9. End-to-end latency dropping from 22.5 to 17.4 ms is not an effect of gpu-unlock
In the same measurement, end-to-end latency (single 30 fps session, p50) dropped by 23% from 22.5 ms to 17.4 ms. Looking at the table alone, it's a tempting number to attribute a 5 ms latency reduction to gpu-unlock. However, GPU kernels only decreased by 0.09 ms, and the worker path only decreased by 0.08 ms. The remaining 5 ms came from elsewhere. We measured a few more things.
The GPU side remained unchanged. Throughout the session, the GPU stayed in the P2 state both before and after, with an SM clock of 1695 MHz, 170W power, and 29–30% utilization. Latency when running three sessions concurrently remained unchanged at 26.5 to 26.3 ms. Since the in-server time reported by the worker (time received -> time answer generated) dropped from 20.3 to 15.9 ms, the difference lies on the server side. Dividing the in-server time after application using a step-measuring wrapper, GPU inference took 11.7 ms, and the preceding CPU phase (JPEG decoding, optical flow tracking for table tennis table, waiting) took 4.2 ms. Since GPU inference was identical before and after, it can be calculated that the CPU phase was about 8.6 ms prior to application.
Re-measuring within the same boot caused the numbers to fluctuate. With gpu-unlock applied, without changing images, scripts, or settings, we measured three more times within a 20-minute window.
| Measurement | gpu-unlock | End-to-end p50 (per session) | In-server p50 | Server CPU usage (total) | Measurement pod CPU | CI build on same server |
|---|---|---|---|---|---|---|
| Pre-application | None | 23.6, 22.5, 22.3 | 20.3 | 7.2% | 0.90 cores | None |
| Post-application | Present | 17.4, 19.1, 17.2 | 15.9 | 7.6% | 0.76 cores | 1.46 cores |
| Re-run 1 | Present | 26.2, 26.2, 26.8 | 23.7 | 9.2% | 0.96 cores | 0.04 cores |
| Re-run 2 | Present | 17.4, 17.2, 17.0 | 15.7 | 8.5% | 0.66 cores | 0.99 cores |
| Re-run 3 | Present | 16.8–18.1 (12 sessions) | 15.4–16.4 | Approx. 9% | 0.5–1.4 cores | 1.0–1.4 cores |
Within the same boot with gpu-unlock enabled, 26.2 ms and 17.2 ms occurred 4 minutes apart. The drop from 22.5 to 17.4 falls within this fluctuation range. Therefore, we can state from actual measurements that this is not an effect of gpu-unlock, and identifying the root cause is a question for the next phase.
Our initial hypothesis was incorrect. After rebooting, the CI server and web pods that were on the same server moved to different servers, so we assumed the cause was "reduced CPU contention." However, the total CPU used by those pods during the measurement before application was about 0.1 cores, and the overall server CPU usage was 7–9% both before and after. Instead, the two slow runs occurred when there was almost no other activity on the server, and the three fast runs occurred when a CI build on the same server was using 1–1.5 cores. When it was slow, the measurement pods used more CPU for the same work (0.90–0.96 cores vs. 0.66–0.76 cores).
This pattern points toward CPU power saving. This server powersave uses the deepest idle state for the governor (C10, taking 680µs to exit), and a single 30fps session works for about 16ms every 33ms and rests for the rest. This suggests that when the entire server is idle, the cores go into a deep sleep or their clocks drop, paying a penalty every time they wake up per frame. To test this, we turned on and off a process running a single core continuously (nice 19) within the same pod while measuring alternately ("Again 3" in the table above), but a CI build was running at that time, so it remained around the 17ms range regardless of whether it was on or off. We waited 30 minutes to measure again once the server became idle, but another build came in immediately, so that opportunity never arrived. Because we could not reproduce the slow state, we leave this explanation as a conjecture.
To verify this, we need to pick a time when the server is idle, repeat the alternate measurements, or measure with pinned cores and restricted C-states (unmeasured).
The takeaway from this task is the procedure rather than the cause itself. When end-to-end numbers improve, do not immediately assign a reason; instead, measure again under the same conditions to first check the range of fluctuation, and divide the work into sub-paths (GPU kernel, worker path, steps within the server) to isolate what changedis the right approach. This time, since the sub-paths remained identical, we were able to rule out gpu-unlock as the cause.
10. How to Revert
These are the methods documented in the repository documentation and application script. We did not revert this time (unexecuted).
| Scope | Method |
|---|---|
| Disable Once | Delete gpu_unlock.targets=... in GRUB editing on the boot screen and boot |
| Disable Permanently | /etc/default/grub Revert sudo update-grub to the pre-application backup and then reboot. Even if the module remains, without this argument it does nothing |
| Remove Completely | Remove the package (Debian-based sudo apt-get remove gpu-unlock-dkms). initramfs is regenerated |
| Verification | Check that the kernel log for the current boot (journalctl -k -b) does not contain gpu_unlock: unlocked and that matrix multiplication BF16 is back to around 70 TFLOPS. |
Since the configuration is written to the registers at boot time, it does not persist on the card. When power is completely turned off, it returns to normal.
11. Summary
| What to Check | Reason (Actual measurements in this post) |
|---|---|
| Measure the share of tensor core kernels in GPU time before applying | The ball-finding model was 4.7%, so the upper bound was −2.3%, and the actual was −0.9% |
| Count by kernel name | Even with TF32 allowed, cuDNN selected convolutions that did not use tensor cores |
| Look at power limit and clock together | It was about 1.95x per clock, but it hit 350W causing the clock to drop by 18%, resulting in 1.61x |
| Maintain a control group | FP32 matrix multiplication (×1.00) and the TF32-disabled model (+0.2%) remained unchanged, allowing us to conclude no other changes interfered |
| Attach configurations for bitwise comparisons | Under deterministic settings, 6 types were identical, but production settings were unmeasured because algorithm selection changed after the configuration was lifted |
| Do not immediately attribute reasons to end-to-end numbers | The 22.5 → 17.4ms change was within the fluctuation range of the same boot (17–26ms), and the sub-paths were identical |
| Observe what else reboots change together | A pending GRUB edit was applied at the same time, further changing the kernel command line |
In short, it was confirmed on this server that this module doubles the FP32 accumulated tensor core speed per clock for consumer cards, and under the power limit, matrix multiplication was roughly 1.6x faster. For workloads where large BF16/FP16 matrix multiplications make up the majority of the time (such as prompt processing or training for language models), speedups will be close to this figure; for tasks bound by memory and the number of kernels, like small convolutional models, there will be little change. You can tell which case it is with a single profile run before applying. In exchange, you take on the burden of unsigned third-party modules touching the GPU boot path and having to directly read the license terms of the NVIDIA firmware.
Titi's Word. I shifted the bicycle gear up by one, and a single pedal turn goes twice as far. However, my leg strength (350W) remained the same, so the pedals turned more slowly, and it ended up being 1.6x faster! Also, finding my ping-pong ball mostly consists of tying shoelaces (memory round-trips), so changing the gear left it almost the same. If I arrive at school early one day, I shouldn't credit the gear, because the wind might have been blowing from behind that day.