TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Unlocking Tensor Cores Real-World Measurement 1: Matrix Multiplication 1.6x, Real-Time Ball Finding 0.9%

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

"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.

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:

  1. The package places the module and firmware into initramfs. Therefore, the module runs before the NVIDIA driver claims the card.
  2. The module only operates when gpu_unlock.targets= is present on the kernel command line. Specify all or a PCI address.
  3. 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 and 0x8 to SS1.
  4. 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.

  1. Matrix multiplication. torch.mm Median 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 are nvidia-smi samples over the same duration (0.5s before application, 100ms while running consecutively for 8s separately after application).
  2. Kernel profile. torch.profiler In 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.
  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.
  4. 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 누적) 처리량, 적용 전과 후 TFLOPS, 7회 중앙값(실측). FP32 는 텐서 코어를 쓰지 않는 대조군 0 40 80 120 BF16 70.1 113.0 ×1.61 FP16 68.4 108.9 ×1.59 TF32 37.2 61.3 ×1.65 FP32 23.8 23.8 ×1.00 적용 전 적용 후 왜 2배가 아닌가: 클록당 처리량은 약 ×1.95 로 늘었지만 350W 전력 상한에 걸려 SM 클록이 약 1710 → 1395MHz 로 내려갔다 (BF16 기준, 적용 전 클록은 0.5초 표본이라 근사)
Figure: TFLOPS per dtype for 8192³ matrix multiplication, before and after application. BF16, FP16, and TF32 are about 1.6x faster, while FP32 remains unchanged.
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.

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

공 찾기 한 프레임의 시간(작업자 안, 배치 1) ms, 반복 3회 중앙값(실측). 상한은 텐서 코어 커널이 절반이 될 때 0 2 4 6 8 10 12 14 적용 전 13.04 적용 후 12.96 상한(계산) 12.83 디코드 RGB 텐서 코어 커널 그 밖의 GPU 커널 추론의 나머지 GPU 커널 9.09 → 9.00ms(−0.9%), 미리 계산한 상한 −2.3% 안 텐서 코어 커널은 0.42 → 0.73ms 로 오히려 늘었다: 풀린 뒤 cuDNN 이 일부 합성곱을 FP32 winograd 대신 TF32 커널로 고쳐 골랐다 추론의 나머지 = 추론 벽시계 − GPU 커널(전송·동기화·후처리)
Figure: Time breakdown of a single ball-finding frame. Pre-application 13.04 ms, post-application 12.96 ms, upper limit when tensor core kernels are halved 12.83 ms.
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.

끝에서 끝 지연은 같은 부팅 안에서도 흔들렸다 30fps 한 세션, 세션마다 p50(ms, 실측). 시각은 KST 14 16 18 20 22 24 26 28 적용 전 15:30 끔 적용 후 15:55 켬 다시 1 16:08 켬 다시 2 16:11 켬 다시 3 16:16 켬 GPU 쪽(클록·전력·커널 시간)은 적용 전과 후가 같았다 느린 두 번은 서버에 다른 일이 거의 없을 때였다(원인은 추정)
Figure: End-to-end latency re-measured within the same boot. Even after application, 26 ms and 17 ms ranges alternated.
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.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment