<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged cuda</title>
<link>https://coderlegion.com/tag/cuda</link>
<description>Powered by Question2Answer</description>
<item>
<title>Reverse-Engineering NVIDIA: Changing a register operand</title>
<link>https://coderlegion.com/28618/reverse-engineering-nvidia-changing-a-register-operand</link>
<description>&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../nvidia-reverse-engineer-gpu-code&quot;&gt;previous article&lt;/a&gt; I have&lt;br&gt;
started reverse-engineering a simple CUDA binary by modifying the value of&lt;br&gt;
a pointer variable.&lt;/p&gt;
&lt;p&gt;After identifying where the value was stored and successfully running the modified CUDA binary, it was time to find out what the corresponding assembly instructions actually looked like at the machine-code level.&lt;/p&gt;
&lt;h3 id=&quot;finding-the-immediate-size&quot;&gt;Finding the immediate size&lt;/h3&gt;
&lt;p&gt;Now, to be able to determine the size and format of the instruction (or at&lt;br&gt;
least the largest constant the instruction from the previous article can hold),&lt;br&gt;
I started to increase the value word-by-word. Instead of simple &lt;code&gt;0xcc&lt;/code&gt; from the&lt;br&gt;
last time, I did:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void test(int *a) {
    a[0] = 0xecc;
}

int main(int argc, const char* argv[]) {
    cudaError_t err;
    int *a;
    cudaMallocManaged(&amp;amp;a, sizeof(int));
    *a = 0x01;
    test&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;(a);
    if ((err = cudaGetLastError()) != cudaSuccess)
        fprintf(stderr, &quot;launch: %s\n&quot;, cudaGetErrorString(err));

    cudaDeviceSynchronize();
    printf(&quot;*a = %x\n&quot;, *a);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And yes, it was there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e out/test-old.sm_50.cubin | grep ecc
00000690: ecc7f000 01000000 05070002 4c980780   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now to try a bit larger value like '0xdecc':&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e out/test-old.sm_50.cubin | grep decc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Searching for the complete &lt;code&gt;0xdecc&lt;/code&gt; pattern didn't find anything. Searching for&lt;br&gt;
the original &lt;code&gt;0xecc&lt;/code&gt; did, however, and the surrounding bytes had changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e out/test-old.sm_50.cubin | grep ecc
00000690: ecc7f000 0100000d 05070002 4c980780   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And there it was. If we compare this line with the hex dump from above, we can&lt;br&gt;
see that we have an extra &lt;code&gt;d&lt;/code&gt; in &lt;code&gt;0100000d&lt;/code&gt;. So I tried increasing the word&lt;br&gt;
size in &lt;code&gt;xxd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep decc
00000690: 0100000decc7f000 4c98078005070002   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the bytes looked much more like a structured &lt;code&gt;64&lt;/code&gt;-bit value rather than an&lt;br&gt;
arbitrary sequence of bytes. The next thing to do was simply increase the test&lt;br&gt;
value and see how much the apparent immediate field could hold.&lt;br&gt;
The result with the largest value was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep ffffffff
00000690: 010ffffffff7f000 4c98078005070002   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, it was impossible to fit more into an &lt;code&gt;int&lt;/code&gt; value as &lt;code&gt;nvcc&lt;/code&gt;&lt;br&gt;
complained about truncation. I naturally tried to find out what will&lt;br&gt;
happen if I use &lt;code&gt;uint64_t&lt;/code&gt; instead of &lt;code&gt;int&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep ffffffff -C1
00000680: 001fc400fe4007f1 010eeeeeeee7f004   ..@.............
00000690: 010ffffffff7f005 eedd200000070204   ............. ..
000006a0: 001f9c00fde007ef 50b0000000070f00   ...............P
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we discovered something. We now had two apparent &lt;code&gt;MOV&lt;/code&gt;s and I wasn't sure&lt;br&gt;
whether they differed because they targeted different registers or because they&lt;br&gt;
loaded the high and low parts of a value.&lt;/p&gt;
&lt;h3 id=&quot;adding-a-static-variable&quot;&gt;Adding a static variable&lt;/h3&gt;
&lt;p&gt;After seeing how the &lt;code&gt;MOV&lt;/code&gt; behaved while changing the value, I tried a little&lt;br&gt;
experiment. I added one more variable with a lot of bytes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdint.h&amp;gt;

__device__ uint64_t x = 0xdaec;

__global__ void test(uint64_t *a) {
        x = 0xccccccccdddddddd;
        a[0] = 0xaaaaaaaabbbbbbbb;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After looking for my usual patterns, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep bbbb -B5 -A7
00000780: 001fc400fe2007f6 4c98078000870001   .. ............L
00000790: 010000000007f002 010000000007f003   ................
000007a0: 001fc000fe4007f1 010dddddddd7f008   ..@.............
000007b0: 010cccccccc7f009 4c98078005070004   ...............L
000007c0: 001fc400fc2007f1 eedd200000070208   .. .......... ..
000007d0: 4c98078005170005 010bbbbbbbb7f006   .......L........
000007e0: 001fbc00fe2007f2 010aaaaaaaa7f007   .. .............
000007f0: eedd200000070406 50b0000000070f00   ..... .........P
00000800: 001ffc00fc4007ef 50b0000000070f00   ..@............P
00000810: 50b0000000070f00 e30000000007000f   .......P........
00000820: 001f8000fc0007ff e2400fffff07000f   ..............@.
00000830: 50b0000000070f00 50b0000000070f00   .......P.......P
00000840: 000000000000daec 0000000000000000   ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking at this we can filter some of these out. We can filter by patterns I&lt;br&gt;
created. Also, I noticed that the line beginning with &lt;code&gt;0xeed&lt;/code&gt; appeared a lot&lt;br&gt;
in my dumps. Here it appeared two times, so I filtered useful stuff out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -c 8 -e -g 8 out/test-old.sm_50.cubin | grep 'bbbb\|cccc\|aaaa\|dddd\|eedd'
000007a8: 010dddddddd7f008   ........
000007b0: 010cccccccc7f009   ........
000007c8: eedd200000070208   ..... ..
000007d8: 010bbbbbbbb7f006   ........
000007e8: 010aaaaaaaa7f007   ........
000007f0: eedd200000070406   ..... ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting part was that the final byte changed systematically as I introduced different values. This made it look increasingly likely that the final byte represented a register operand.&lt;/p&gt;
&lt;p&gt;This is current test output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./test-old 
*a = aaaaaaaabbbbbbbb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can we make it display the value of variable &lt;code&gt;x&lt;/code&gt;? Let's try hacking &lt;code&gt;0xeed2&lt;/code&gt;&lt;br&gt;
instructions. My hypothesis was that the low bits of these instructions encode&lt;br&gt;
the register operand, similarly to what we observed in the &lt;code&gt;0x10&lt;/code&gt; lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -c 8 -e -g 8 test-old | grep eedd2
0008a848: eedd200000070208   ..... ..
0008a870: eedd200000070406   ..... ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I used &lt;code&gt;bvi&lt;/code&gt; to change the final byte of the second instruction from &lt;code&gt;06&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;08&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;xxd -c 8 -e -g 8 test-old | grep eedd2
0008a848: eedd200000070208   ..... ..
0008a870: eedd200000070408   ..... ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And (not) surprisingly, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./test-old
*a = ccccccccdddddddd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We actually switched registers from which the value for &lt;code&gt;a[0]&lt;/code&gt; was loaded.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;For now we can identify (and modify to a certain extent) two instructions:&lt;/p&gt;
&lt;p&gt;The first instruction appears to be a &lt;code&gt;MOV&lt;/code&gt;-like instruction, with a format that&lt;br&gt;
currently looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;010VVVVVVVV7f00R
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;VVVVVVVV&lt;/code&gt; appears to represent an immediate value, while &lt;code&gt;R&lt;/code&gt; appears to&lt;br&gt;
identify the destination register.&lt;/p&gt;
&lt;p&gt;The second instruction appears to select a register containing the value that is&lt;br&gt;
ultimately written to &lt;code&gt;a[0]&lt;/code&gt;. Its encoding currently appears to be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;eedX200000070Y0R
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where &lt;code&gt;R&lt;/code&gt; appears to identify the register. &lt;code&gt;X/Y&lt;/code&gt; remain unknown; I suspect one&lt;br&gt;
of them encodes information about the operand size, since I observed different&lt;br&gt;
values when experimenting with 32-bit &lt;code&gt;int&lt;/code&gt; versus 64-bit &lt;code&gt;uint64_t&lt;/code&gt; values.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28618/reverse-engineering-nvidia-changing-a-register-operand</guid>
<pubDate>Fri, 25 Sep 2026 14:41:55 +0000</pubDate>
</item>
<item>
<title>GPU-Accelerating MSCRED with CUDA, im2col, GEMM, and a Custom PyTorch Extension</title>
<link>https://coderlegion.com/26883/gpu-accelerating-mscred-with-cuda-im2col-gemm-and-a-custom-pytorch-extension</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;During my M.Tech in Data Science and Artificial Intelligence at IIT Bhilai (2021–2023), I conducted thesis research on multivariate time-series anomaly detection and GPU acceleration using industrial process signals.&lt;/p&gt;
&lt;p&gt;My thesis, titled &lt;strong&gt;“Anomaly Detection of Multivariate Time Series Data and Acceleration Using GPUs,”&lt;/strong&gt; explored an MSCRED—Multi-Scale Convolutional Recurrent Encoder-Decoder—and ConvLSTM-based pipeline for modelling relationships between signals and detecting abnormal temporal patterns.&lt;/p&gt;
&lt;p&gt;Although the model produced useful anomaly-detection results, its convolution-heavy encoder created a significant CPU bottleneck. I therefore implemented GPU-accelerated operations using CUDA, transformed convolution into an &lt;code&gt;im2col&lt;/code&gt; plus GEMM workflow, investigated global-memory and shared-memory implementations, and integrated the accelerated operations with PyTorch.&lt;/p&gt;
&lt;p&gt;In the measured workload, encoder runtime decreased from &lt;strong&gt;300 seconds on the CPU to 23.664 seconds on the GPU&lt;/strong&gt;, representing an approximately &lt;strong&gt;12.68× encoder speed-up&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Research attribution:&lt;/strong&gt; MSCRED and ConvLSTM are established architectures introduced by the researchers cited in the References section. This article describes how I used, adapted, profiled, and accelerated selected operations as part of my M.Tech thesis.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Data confidentiality:&lt;/strong&gt; The original research involved industrial process data. This article discusses the architecture, optimisation methodology, and aggregate benchmark results without publishing proprietary signals, internal process details, confidential datasets, or restricted source code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-anomaly-detection-problem&quot;&gt;The Anomaly-Detection Problem&lt;/h3&gt;
&lt;p&gt;Industrial systems often generate many related sensor and process-control signals at the same time. A single signal may appear normal when viewed independently, while the relationship between several signals may indicate an abnormal operating condition.&lt;/p&gt;
&lt;p&gt;This makes multivariate time-series anomaly detection more difficult than analysing one signal at a time.&lt;/p&gt;
&lt;p&gt;The model must learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Relationships between multiple signals&lt;/li&gt;
&lt;li&gt;Changes across different time windows&lt;/li&gt;
&lt;li&gt;Temporal dependencies between successive observations&lt;/li&gt;
&lt;li&gt;Deviations from normal operating patterns&lt;/li&gt;
&lt;li&gt;Reconstruction differences that may indicate anomalies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal of the research was to detect unusual multivariate patterns while preserving the temporal and spatial relationships present in the data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mscred-and-convlstm-architecture&quot;&gt;MSCRED and ConvLSTM Architecture&lt;/h3&gt;
&lt;p&gt;MSCRED models relationships between multiple time-series signals by constructing &lt;strong&gt;signature matrices&lt;/strong&gt; over different temporal windows.&lt;/p&gt;
&lt;p&gt;At a high level, the pipeline contains:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Signature-matrix generation from multivariate signals&lt;/li&gt;
&lt;li&gt;A convolutional encoder for spatial feature extraction&lt;/li&gt;
&lt;li&gt;ConvLSTM layers for temporal modelling&lt;/li&gt;
&lt;li&gt;A decoder that reconstructs expected signal relationships&lt;/li&gt;
&lt;li&gt;Reconstruction-error analysis for anomaly scoring&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The encoder processes the signature matrices through convolutional operations, while ConvLSTM captures how the encoded representations evolve over time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Multivariate signals
        ↓
Signature matrices
        ↓
CNN encoder
        ↓
ConvLSTM
        ↓
Decoder
        ↓
Reconstruction error
        ↓
Anomaly score
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-signature-matrices-are-useful&quot;&gt;Why Signature Matrices Are Useful&lt;/h4&gt;
&lt;p&gt;A signature matrix represents pairwise relationships between signals over a selected time interval.&lt;/p&gt;
&lt;p&gt;By generating these matrices at multiple scales, the model can observe both short-term and longer-term relationships. This helps identify anomalies that may not be visible in an individual signal.&lt;/p&gt;
&lt;h4 id=&quot;role-of-the-encoder&quot;&gt;Role of the Encoder&lt;/h4&gt;
&lt;p&gt;The convolutional encoder extracts spatial patterns from the signature matrices. It reduces the raw matrix representation into compact feature maps that capture important relationships between signals.&lt;/p&gt;
&lt;h4 id=&quot;role-of-convlstm&quot;&gt;Role of ConvLSTM&lt;/h4&gt;
&lt;p&gt;ConvLSTM models how those spatial representations change over time.&lt;/p&gt;
&lt;p&gt;Unlike a standard fully connected LSTM, ConvLSTM preserves the spatial structure of feature maps while learning temporal dependencies.&lt;/p&gt;
&lt;h4 id=&quot;role-of-the-decoder&quot;&gt;Role of the Decoder&lt;/h4&gt;
&lt;p&gt;The decoder reconstructs the expected signature matrices.&lt;/p&gt;
&lt;p&gt;When the reconstructed output differs significantly from the observed input, the reconstruction error can indicate an anomalous pattern.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;anomaly-detection-evaluation&quot;&gt;Anomaly-Detection Evaluation&lt;/h3&gt;
&lt;p&gt;The purpose of the MSCRED and ConvLSTM pipeline was to detect abnormal relationships and temporal behaviour across multiple industrial process signals.&lt;/p&gt;
&lt;p&gt;The model learned normal multivariate patterns by reconstructing signature matrices. During evaluation, the difference between the observed signature matrices and the reconstructed outputs was used to calculate a reconstruction error.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Observed signature matrix
            ↓
MSCRED reconstruction
            ↓
Reconstruction error
            ↓
Thresholding
            ↓
Normal or anomalous
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A larger reconstruction error indicated that the observed relationships differed from patterns learned during normal operation and could therefore represent an anomaly.&lt;/p&gt;
&lt;p&gt;The thesis evaluated anomaly-detection quality using the F1 score. Under the reported experimental setup, the &lt;strong&gt;best recorded F1 score was 0.941&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This result represents model-quality evaluation and should be interpreted separately from the CUDA runtime measurements presented later. The GPU memory optimisations were designed to reduce computation time, not to improve predictive accuracy.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-the-performance-bottleneck-appeared&quot;&gt;Where the Performance Bottleneck Appeared&lt;/h3&gt;
&lt;p&gt;The encoder repeatedly applied convolutional operations to signature matrices.&lt;/p&gt;
&lt;p&gt;On the CPU, these operations involved substantial nested computation across:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input channels&lt;/li&gt;
&lt;li&gt;Output filters&lt;/li&gt;
&lt;li&gt;Spatial positions&lt;/li&gt;
&lt;li&gt;Kernel dimensions&lt;/li&gt;
&lt;li&gt;Batch elements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This made the encoder a strong candidate for GPU acceleration because many convolution calculations could be performed in parallel.&lt;/p&gt;
&lt;p&gt;The objective was not to redesign the anomaly-detection model. Instead, it was to preserve its expected computational behaviour while accelerating selected expensive operations.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;transforming-conv2d-into-im2col-and-gemm&quot;&gt;Transforming Conv2D into &lt;code&gt;im2col&lt;/code&gt; and GEMM&lt;/h3&gt;
&lt;p&gt;A convolution repeatedly applies a kernel to overlapping regions of an input feature map.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;im2col&lt;/code&gt;, or image-to-column, transformation rearranges those local regions into columns of a matrix.&lt;/p&gt;
&lt;p&gt;After this transformation, convolution can be expressed as a general matrix multiplication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Output matrix = Weight matrix × im2col(Input)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This conversion is useful because matrix multiplication is highly parallel and can be executed efficiently on a GPU.&lt;/p&gt;
&lt;h4 id=&quot;conceptual-transformation&quot;&gt;Conceptual Transformation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Input feature map
        ↓
Extract overlapping kernel windows
        ↓
Arrange windows as matrix columns
        ↓
Multiply by reshaped filter matrix
        ↓
Reshape result into output feature map
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-use-gemm-&quot;&gt;Why Use GEMM?&lt;/h4&gt;
&lt;p&gt;GEMM stands for &lt;strong&gt;General Matrix-Matrix Multiplication&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Modern GPU architectures are designed to perform large numbers of multiply-and-accumulate operations in parallel. Expressing convolution as GEMM allows the workload to use that parallelism.&lt;/p&gt;
&lt;h4 id=&quot;trade-off&quot;&gt;Trade-Off&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;im2col&lt;/code&gt; introduces additional memory usage because overlapping input regions are copied into an intermediate matrix.&lt;/p&gt;
&lt;p&gt;The method is most effective when the computational benefit of parallel matrix multiplication outweighs the cost of creating and storing the intermediate representation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;cuda-kernel-design&quot;&gt;CUDA Kernel Design&lt;/h3&gt;
&lt;p&gt;The CUDA implementation divided the computation across many GPU threads.&lt;/p&gt;
&lt;p&gt;Each thread was responsible for part of the output, while thread blocks grouped related work.&lt;/p&gt;
&lt;p&gt;Important kernel-design considerations included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mapping output elements to threads&lt;/li&gt;
&lt;li&gt;Selecting thread-block dimensions&lt;/li&gt;
&lt;li&gt;Coalescing memory access where possible&lt;/li&gt;
&lt;li&gt;Reducing unnecessary global-memory operations&lt;/li&gt;
&lt;li&gt;Avoiding excessive synchronisation&lt;/li&gt;
&lt;li&gt;Preserving correct tensor indexing&lt;/li&gt;
&lt;li&gt;Managing intermediate buffers efficiently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implementation required careful validation because incorrect indexing in GPU code can produce plausible-looking but incorrect outputs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;global-memory-and-shared-memory&quot;&gt;Global Memory and Shared Memory&lt;/h3&gt;
&lt;p&gt;The first GPU implementation relied primarily on &lt;strong&gt;global memory&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Global memory provides access to the complete input, but its latency is considerably higher than that of on-chip shared memory.&lt;/p&gt;
&lt;p&gt;The optimised implementation staged reusable tiles inside &lt;strong&gt;shared memory&lt;/strong&gt;. Threads within the same block could then reuse nearby values without repeatedly loading them from global memory.&lt;/p&gt;
&lt;p&gt;The shared-memory optimisation was intended to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce repeated global-memory reads&lt;/li&gt;
&lt;li&gt;Improve data locality&lt;/li&gt;
&lt;li&gt;Increase data reuse within each thread block&lt;/li&gt;
&lt;li&gt;Reduce memory-related kernel overhead&lt;/li&gt;
&lt;li&gt;Improve matrix-multiplication efficiency&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;conceptual-data-flow&quot;&gt;Conceptual Data Flow&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Global memory
      ↓
Load reusable tile
      ↓
Shared memory
      ↓
Threads perform repeated calculations
      ↓
Write result to global memory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shared memory is limited in capacity, so tile dimensions and thread-block organisation must be selected carefully.&lt;/p&gt;
&lt;p&gt;Poorly selected configurations can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce occupancy&lt;/li&gt;
&lt;li&gt;Increase synchronisation overhead&lt;/li&gt;
&lt;li&gt;Create bank conflicts&lt;/li&gt;
&lt;li&gt;Use too much shared memory per block&lt;/li&gt;
&lt;li&gt;Limit the number of active thread blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;integrating-the-cuda-implementation-with-pytorch&quot;&gt;Integrating the CUDA Implementation with PyTorch&lt;/h3&gt;
&lt;p&gt;To use the accelerated operations inside the existing model pipeline, I connected the CUDA implementation to PyTorch through a custom extension.&lt;/p&gt;
&lt;p&gt;The integration enabled PyTorch tensors to be passed to the CUDA implementation and the computed results to be returned to the model pipeline.&lt;/p&gt;
&lt;p&gt;Important integration concerns included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tensor dimensions&lt;/li&gt;
&lt;li&gt;Device placement&lt;/li&gt;
&lt;li&gt;Data types&lt;/li&gt;
&lt;li&gt;Contiguous-memory assumptions&lt;/li&gt;
&lt;li&gt;Compilation and linking&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;CPU-versus-GPU output comparison&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Correctness validation was essential. A faster kernel is not useful if it changes tensor shapes, produces incorrect indexing, introduces unacceptable numerical differences, or behaves inconsistently for the tested configurations.&lt;/p&gt;
&lt;p&gt;The custom extension made it possible to retain PyTorch for model development while using lower-level CUDA operations for selected performance-critical components.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;experimental-setup&quot;&gt;Experimental Setup&lt;/h3&gt;
&lt;p&gt;The benchmark used the following environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU:&lt;/strong&gt; NVIDIA RTX A6000, 48 GB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; 16-core Intel Xeon Gold 5218&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System memory:&lt;/strong&gt; 64 GB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data points:&lt;/strong&gt; 36,000&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number of features:&lt;/strong&gt; 30&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch size:&lt;/strong&gt; 128&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU threads:&lt;/strong&gt; 32&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;timing-methodology&quot;&gt;Timing Methodology&lt;/h4&gt;
&lt;p&gt;The timings reported here are measurements from my thesis experiments using the specified hardware and workload. The CPU and GPU implementations used the same input dimensions and batch configuration.&lt;/p&gt;
&lt;p&gt;The results should be treated as comparative experimental measurements rather than universal library benchmarks. Performance may vary with implementation details, CUDA configuration, tensor dimensions, data-transfer costs, compiler settings, and hardware.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;performance-benchmark-results&quot;&gt;Performance Benchmark Results&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &lt;/th&gt;&lt;th&gt;CPU &lt;/th&gt;&lt;th&gt;GPU: global memory &lt;/th&gt;&lt;th&gt;GPU: shared memory&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Encoder &lt;/td&gt;&lt;td&gt;300 s &lt;/td&gt;&lt;td&gt;27.492 s &lt;/td&gt;&lt;td&gt;23.664 s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ConvLSTM &lt;/td&gt;&lt;td&gt;1,200 s &lt;/td&gt;&lt;td&gt;909.208 s &lt;/td&gt;&lt;td&gt;847.260 s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Total runtime &lt;/td&gt;&lt;td&gt;2,700 s &lt;/td&gt;&lt;td&gt;1,594 s &lt;/td&gt;&lt;td&gt;1,481 s&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;results-at-a-glance&quot;&gt;Results at a Glance&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Anomaly-detection quality:&lt;/strong&gt; best recorded F1 score of &lt;strong&gt;0.941&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encoder acceleration:&lt;/strong&gt; approximately &lt;strong&gt;12.68×&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-end acceleration:&lt;/strong&gt; approximately &lt;strong&gt;1.82×&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total runtime reduction:&lt;/strong&gt; approximately &lt;strong&gt;45.1%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shared-memory implementation reduced encoder runtime from &lt;strong&gt;300 seconds to 23.664 seconds&lt;/strong&gt;, producing an approximately &lt;strong&gt;12.68× encoder speed-up&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For the complete measured pipeline, runtime decreased from &lt;strong&gt;2,700 seconds to 1,481 seconds&lt;/strong&gt;. The total-runtime measurement represents the complete measured pipeline rather than only the encoder and ConvLSTM components shown separately.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-the-encoder-improved-more-than-convlstm&quot;&gt;Why the Encoder Improved More Than ConvLSTM&lt;/h3&gt;
&lt;p&gt;The convolutional encoder benefited strongly from GPU execution because its operations provided substantial data parallelism.&lt;/p&gt;
&lt;p&gt;ConvLSTM achieved a smaller improvement because of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Recurrent dependencies&lt;/li&gt;
&lt;li&gt;Sequential temporal processing&lt;/li&gt;
&lt;li&gt;Memory movement&lt;/li&gt;
&lt;li&gt;Repeated state updates&lt;/li&gt;
&lt;li&gt;More limited parallelism across time steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consequently, the &lt;strong&gt;12.68× encoder acceleration&lt;/strong&gt; did not translate into a &lt;strong&gt;12.68× end-to-end speed-up&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After the encoder was accelerated, ConvLSTM became the dominant runtime bottleneck.&lt;/p&gt;
&lt;p&gt;This is an important performance-engineering lesson: accelerating one component can move the bottleneck to another part of the system.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;correctness-validation&quot;&gt;Correctness Validation&lt;/h3&gt;
&lt;p&gt;The CUDA implementation was compared with the reference implementation for the tested tensor shapes and configurations.&lt;/p&gt;
&lt;p&gt;The optimisation work focused on reducing execution time while preserving the expected computational behaviour. Model-quality evaluation and runtime evaluation were treated as separate concerns.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;engineering-challenges&quot;&gt;Engineering Challenges&lt;/h3&gt;
&lt;p&gt;Several engineering challenges appeared during the implementation.&lt;/p&gt;
&lt;h4 id=&quot;1-correct-tensor-indexing&quot;&gt;1. Correct Tensor Indexing&lt;/h4&gt;
&lt;p&gt;The CUDA kernel had to reproduce the same logical output as the reference convolution operation.&lt;/p&gt;
&lt;p&gt;A small indexing error could generate incorrect feature maps without necessarily causing a runtime failure.&lt;/p&gt;
&lt;h4 id=&quot;2-memory-management&quot;&gt;2. Memory Management&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;im2col&lt;/code&gt; transformation created additional intermediate data.&lt;/p&gt;
&lt;p&gt;This required careful allocation, reuse, and release of GPU memory.&lt;/p&gt;
&lt;h4 id=&quot;3-synchronisation&quot;&gt;3. Synchronisation&lt;/h4&gt;
&lt;p&gt;Threads sharing data through shared memory had to synchronise at appropriate points.&lt;/p&gt;
&lt;p&gt;Missing or unnecessary synchronisation could cause incorrect results or reduced performance.&lt;/p&gt;
&lt;h4 id=&quot;4-kernel-configuration&quot;&gt;4. Kernel Configuration&lt;/h4&gt;
&lt;p&gt;Thread-block dimensions and tile sizes affected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Occupancy&lt;/li&gt;
&lt;li&gt;Shared-memory usage&lt;/li&gt;
&lt;li&gt;Memory access&lt;/li&gt;
&lt;li&gt;Work distribution&lt;/li&gt;
&lt;li&gt;Overall kernel performance&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;5-pytorch-integration&quot;&gt;5. PyTorch Integration&lt;/h4&gt;
&lt;p&gt;The extension had to account for tensor shapes, data types, device placement, memory layout, compilation, linking, and output comparison.&lt;/p&gt;
&lt;h4 id=&quot;6-measuring-the-correct-bottleneck&quot;&gt;6. Measuring the Correct Bottleneck&lt;/h4&gt;
&lt;p&gt;Improving one component changed the relative cost of other components.&lt;/p&gt;
&lt;p&gt;Profiling therefore had to be repeated after major optimisations.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;engineering-lessons&quot;&gt;Engineering Lessons&lt;/h3&gt;
&lt;h4 id=&quot;1-optimising-one-component-does-not-guarantee-an-equivalent-end-to-end-improvement&quot;&gt;1. Optimising One Component Does Not Guarantee an Equivalent End-to-End Improvement&lt;/h4&gt;
&lt;p&gt;Once the encoder was accelerated, ConvLSTM represented a larger proportion of the remaining runtime.&lt;/p&gt;
&lt;h4 id=&quot;2-im2col-makes-convolution-gpu-friendly-but-introduces-memory-overhead&quot;&gt;2. &lt;code&gt;im2col&lt;/code&gt; Makes Convolution GPU-Friendly but Introduces Memory Overhead&lt;/h4&gt;
&lt;p&gt;The transformation enables GEMM-based execution while creating a larger intermediate representation.&lt;/p&gt;
&lt;h4 id=&quot;3-shared-memory-is-valuable-when-threads-repeatedly-reuse-nearby-data&quot;&gt;3. Shared Memory Is Valuable When Threads Repeatedly Reuse Nearby Data&lt;/h4&gt;
&lt;p&gt;Its benefit depends on access patterns, tile sizes, synchronisation, and thread-block configuration.&lt;/p&gt;
&lt;h4 id=&quot;4-profiling-must-guide-optimisation&quot;&gt;4. Profiling Must Guide Optimisation&lt;/h4&gt;
&lt;p&gt;Performance work should focus on measured bottlenecks rather than assumptions about which component is slow.&lt;/p&gt;
&lt;h4 id=&quot;5-correctness-is-as-important-as-speed&quot;&gt;5. Correctness Is as Important as Speed&lt;/h4&gt;
&lt;p&gt;GPU output should be compared against a trusted reference implementation before performance improvements are accepted.&lt;/p&gt;
&lt;h4 id=&quot;6-custom-pytorch-extensions-offer-flexibility-at-the-cost-of-additional-complexity&quot;&gt;6. Custom PyTorch Extensions Offer Flexibility at the Cost of Additional Complexity&lt;/h4&gt;
&lt;p&gt;Device handling, tensor shapes, data types, compilation, numerical validation, and fallback behaviour all require careful attention.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;what-is-mscred-&quot;&gt;What is MSCRED?&lt;/h4&gt;
&lt;p&gt;MSCRED stands for &lt;strong&gt;Multi-Scale Convolutional Recurrent Encoder-Decoder&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It is an architecture designed to model relationships between multiple time-series signals and detect abnormal behaviour through reconstruction error.&lt;/p&gt;
&lt;h4 id=&quot;what-does-im2col-do-&quot;&gt;What does &lt;code&gt;im2col&lt;/code&gt; do?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;im2col&lt;/code&gt; rearranges overlapping convolution windows into matrix columns, allowing convolution to be expressed as matrix multiplication.&lt;/p&gt;
&lt;h4 id=&quot;why-did-shared-memory-help-&quot;&gt;Why did shared memory help?&lt;/h4&gt;
&lt;p&gt;Shared memory allowed threads in the same block to reuse data without repeatedly loading it from slower global memory. This reduced memory-access overhead for reusable tiles.&lt;/p&gt;
&lt;h4 id=&quot;did-gpu-memory-optimisation-improve-the-f1-score-&quot;&gt;Did GPU memory optimisation improve the F1 score?&lt;/h4&gt;
&lt;p&gt;That was not the purpose of the optimisation, and this article does not claim that it did.&lt;/p&gt;
&lt;p&gt;The best recorded F1 score describes anomaly-detection quality in the thesis experiments. The CPU and GPU timing results describe computational performance. They should be interpreted separately.&lt;/p&gt;
&lt;h4 id=&quot;why-was-the-encoder-speed-up-much-larger-than-the-total-speed-up-&quot;&gt;Why was the encoder speed-up much larger than the total speed-up?&lt;/h4&gt;
&lt;p&gt;After the encoder became faster, ConvLSTM represented a larger share of total runtime. The overall system was therefore limited by the remaining bottleneck.&lt;/p&gt;
&lt;h4 id=&quot;is-a-custom-cuda-kernel-always-faster-than-pytorch-operations-&quot;&gt;Is a Custom CUDA Kernel Always Faster Than PyTorch Operations?&lt;/h4&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Optimised PyTorch and CUDA libraries are already highly efficient for many operations. A custom kernel is most useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The workload has a specialised access pattern&lt;/li&gt;
&lt;li&gt;Existing operations create unnecessary overhead&lt;/li&gt;
&lt;li&gt;Profiling identifies a clear bottleneck&lt;/li&gt;
&lt;li&gt;Correctness and maintenance costs are justified&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;can-the-original-industrial-dataset-be-shared-&quot;&gt;Can the Original Industrial Dataset Be Shared?&lt;/h4&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;The original dataset contains industrial process information. A public reproduction would require synthetic or appropriately anonymised multivariate time-series data.&lt;/p&gt;
&lt;h4 id=&quot;are-these-benchmark-results-universal-&quot;&gt;Are These Benchmark Results Universal?&lt;/h4&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Performance depends on GPU architecture, CPU configuration, tensor dimensions, batch size, kernel implementation, memory layout, CUDA and PyTorch versions, compiler settings, and workload characteristics.&lt;/p&gt;
&lt;p&gt;The reported numbers apply to the specific experimental setup described in this article.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;limitations-and-data-privacy-considerations&quot;&gt;Limitations and Data-Privacy Considerations&lt;/h3&gt;
&lt;p&gt;The reported results were obtained on a specific hardware configuration and workload.&lt;/p&gt;
&lt;p&gt;Different GPUs, tensor dimensions, batch sizes, memory layouts, CUDA versions, and kernel configurations may produce different results.&lt;/p&gt;
&lt;p&gt;The original dataset cannot be published because it contains industrial process information. A public reproduction would therefore require synthetic or appropriately anonymised multivariate time-series data.&lt;/p&gt;
&lt;p&gt;This article presents aggregate benchmark measurements and technical methodology rather than the complete industrial dataset or restricted implementation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;future-work&quot;&gt;Future Work&lt;/h3&gt;
&lt;p&gt;Future work could focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Profiling and optimising the ConvLSTM component&lt;/li&gt;
&lt;li&gt;Reducing intermediate memory allocation&lt;/li&gt;
&lt;li&gt;Reducing host-to-device and device-to-device memory movement&lt;/li&gt;
&lt;li&gt;Comparing the custom implementation with optimised CUDA libraries&lt;/li&gt;
&lt;li&gt;Evaluating additional tile and thread-block configurations&lt;/li&gt;
&lt;li&gt;Testing across different GPU architectures&lt;/li&gt;
&lt;li&gt;Reproducing the workflow using synthetic public data&lt;/li&gt;
&lt;li&gt;Adding automated correctness and regression tests&lt;/li&gt;
&lt;li&gt;Measuring kernel-level execution using GPU profiling tools&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This work was completed as part of my M.Tech thesis research at the &lt;strong&gt;Indian Institute of Technology Bhilai&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It demonstrated how low-level GPU optimisation can be integrated into a deep-learning anomaly-detection pipeline.&lt;/p&gt;
&lt;p&gt;The convolutional encoder benefited substantially from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CUDA parallelisation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;im2col&lt;/code&gt;-based lowering&lt;/li&gt;
&lt;li&gt;GEMM&lt;/li&gt;
&lt;li&gt;Shared-memory data reuse&lt;/li&gt;
&lt;li&gt;Custom PyTorch integration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Encoder runtime decreased from &lt;strong&gt;300 seconds to 23.664 seconds&lt;/strong&gt; in the measured workload.&lt;/p&gt;
&lt;p&gt;The broader result also illustrated an important systems principle: once one stage is accelerated, another stage may become the new performance bottleneck.&lt;/p&gt;
&lt;p&gt;In this case, ConvLSTM limited the end-to-end improvement even after the encoder achieved a substantial speed-up.&lt;/p&gt;
&lt;p&gt;The work reinforced that successful performance engineering requires both low-level optimisation and system-level measurement.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Zhang, C., Song, D., Chen, Y., Feng, X., Lumezanu, C., Cheng, W., Ni, J., Zong, B., Chen, H., and Chawla, N. V. (2019). &lt;strong&gt;A Deep Neural Network for Unsupervised Anomaly Detection and Diagnosis in Multivariate Time Series Data.&lt;/strong&gt; &lt;em&gt;Proceedings of the AAAI Conference on Artificial Intelligence&lt;/em&gt;, 33(01), 1409–1416. &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1609/aaai.v33i01.33011409&quot;&gt;https://doi.org/10.1609/aaai.v33i01.33011409&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shi, X., Chen, Z., Wang, H., Yeung, D.-Y., Wong, W.-K., and Woo, W.-C. (2015). &lt;strong&gt;Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting.&lt;/strong&gt; &lt;em&gt;Advances in Neural Information Processing Systems 28&lt;/em&gt;, 802–810. &lt;a rel=&quot;nofollow&quot; href=&quot;https://proceedings.neurips.cc/paper_files/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.html&quot;&gt;https://proceedings.neurips.cc/paper_files/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NVIDIA. &lt;strong&gt;CUDA C++ Programming Guide.&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.nvidia.com/cuda/cuda-c-programming-guide/&quot;&gt;https://docs.nvidia.com/cuda/cuda-c-programming-guide/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PyTorch. &lt;strong&gt;Custom C++ and CUDA Operators.&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.pytorch.org/tutorials/advanced/cpp_custom_ops.html&quot;&gt;https://docs.pytorch.org/tutorials/advanced/cpp_custom_ops.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;citation-and-feedback&quot;&gt;Citation and Feedback&lt;/h3&gt;
&lt;p&gt;If this article informs academic or technical work, please cite the original MSCRED and ConvLSTM papers above.&lt;/p&gt;
&lt;p&gt;To refer specifically to my thesis implementation, optimisation process, or reported benchmarks, you may cite this article as:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Vutnoor, R. (2026). “GPU-Accelerating MSCRED with CUDA, im2col, GEMM, and a Custom PyTorch Extension.” DEV Community.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you found the article useful, a reaction, comment, or constructive technical question on DEV would be appreciated.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I am &lt;strong&gt;Ranjith Vutnoor&lt;/strong&gt;, an AI/ML Software Engineer and IIT Bhilai alumnus working on production RAG systems, LLM evaluation, model fine-tuning, PyTorch, and GPU-accelerated machine learning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ranjithvutnoor.github.io/portfolio/&quot;&gt;Portfolio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ranjithvutnoor&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/ranjithvutnoor/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26883/gpu-accelerating-mscred-with-cuda-im2col-gemm-and-a-custom-pytorch-extension</guid>
<pubDate>Fri, 11 Sep 2026 18:07:01 +0000</pubDate>
</item>
<item>
<title>Reverse-Engineering NVIDIA: Modifying a CUDA binary</title>
<link>https://coderlegion.com/26347/reverse-engineering-nvidia-modifying-a-cuda-binary</link>
<description>&lt;h3 id=&quot;motivation-1&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;For quite a while I wanted to find out how NVIDIA really works under the hood.&lt;br&gt;
One of the greatest unknowns for me was how GPU code is compiled, packaged and&lt;br&gt;
loaded. So I accepted my own challenge: let's try and decode machine code&lt;br&gt;
running on GPU.&lt;/p&gt;
&lt;h3 id=&quot;test-code&quot;&gt;Test code&lt;/h3&gt;
&lt;p&gt;I needed to start really simple and to get as much data as possible. My first&lt;br&gt;
CUDA program to try and decipher was simply:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void test(int *a) {
        a[0] = 0x00;
}

int main(int argc, const char* argv[]) {
        cudaError_t err;
        int *a;
        cudaMallocManaged(&amp;amp;a, sizeof(int));
        *a = 0x01;
        test&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;(a);
        if ((err = cudaGetLastError()) != cudaSuccess)
                fprintf(stderr, &quot;launch: %s\n&quot;, cudaGetErrorString(err));

        cudaDeviceSynchronize();
        printf(&quot;*a = %x\n&quot;, *a);

        return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I won't get into too many details here (I am not a CUDA expert), but essentially, the function marked with &lt;code&gt;__global__&lt;/code&gt; is the kernel that will run on the device. The &lt;code&gt;cudaMallocManaged()&lt;/code&gt; function takes care of allocating memory that can be accessed by both the host and the device, saving us some additional memory-management code. The &lt;code&gt;test&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;(a)&lt;/code&gt; syntax launches the &lt;code&gt;test&lt;/code&gt; kernel with one block containing one thread, while the CUDA runtime takes care of loading the kernel for execution on the GPU. Finally, &lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt; waits for the GPU to finish executing the kernel, ensuring that the result written to &lt;code&gt;a&lt;/code&gt; is available when we access it from the host.&lt;/p&gt;
&lt;h4 id=&quot;build&quot;&gt;Build&lt;/h4&gt;
&lt;p&gt;I also created a reusable build command that will also give me something to&lt;br&gt;
start with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;nvcc -arch=sm_50 -lineinfo --keep --keep-dir out -o test test.cu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: for this to work, the &lt;code&gt;out&lt;/code&gt; directory needs to be created before running&lt;br&gt;
&lt;code&gt;nvcc&lt;/code&gt; compiler. Now I got a folder full of really useful files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ ls out/
test.cpp1.ii    test.cudafe1.cpp     test_dlink.fatbin    test_dlink.reg.c        test.fatbin.c   test.ptx
test.cpp4.ii    test.cudafe1.gpu     test_dlink.fatbin.c  test_dlink.sm_50.cubin  test.module_id  test.sm_50.cubin
test.cudafe1.c  test.cudafe1.stub.c  test_dlink.o         test.fatbin             test.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;test.sm_50.cubin&lt;/code&gt; file sounded like exactly what I was looking for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ file out/test.sm_50.cubin 
out/test.sm_50.cubin: ELF 64-bit LSB executable, NVIDIA CUDA architecture,, statically linked, not stripped
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;first-hack&quot;&gt;First hack&lt;/h3&gt;
&lt;p&gt;Now I wanted to try changing the value of &lt;code&gt;a&lt;/code&gt; in the &lt;code&gt;test&lt;/code&gt; function and see how&lt;br&gt;
&lt;code&gt;cubin&lt;/code&gt; file will behave. I had no idea what I was dealing with, so I started&lt;br&gt;
checking the file with random small values to get the one with least repetitions&lt;br&gt;
in the CUDA binary. After some random attempts I realized that the best one is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump out/test.sm_50.cubin | grep bc
0000680 07f1 fde0 bc00 001f 02ff 0007 2000 eedc
00008f0 0000 0000 0000 0000 04bc 0000 0000 0000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I decided to change my test function to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;__global__ void test(int *a) {
        a[0] = 0xbc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything went well, I would see an additional line here containing my &lt;code&gt;bc&lt;/code&gt;.&lt;br&gt;
The reason I started with really small values is because I had no idea what kind&lt;br&gt;
of byte ordering I will encounter. After rebuilding the binary, I now got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump out/test.sm_50.cubin | grep bc
0000310 f1bc 06d4 02fc 0000 0209 0000 0000 0000
0000690 f000 0bc7 0000 0100 0002 0507 0780 4c98
00006a0 07f2 fe20 bc00 001f 0003 0517 0780 4c98
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well, I definitely got a bit more different hex dump. So I tried just changing&lt;br&gt;
it again, this time to &lt;code&gt;0xcc&lt;/code&gt; (just randomly) to see what I will get. And I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump out/test.sm_50.cubin | grep cc
0000690 f000 0cc7 0000 0100 0002 0507 0780 4c98
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was promising. Notice that this is the same as address &lt;code&gt;0x0000690&lt;/code&gt; in the&lt;br&gt;
previous run, except we got &lt;code&gt;0cc7&lt;/code&gt; instead of &lt;code&gt;0bc7&lt;/code&gt;. Now I could try and find&lt;br&gt;
this kind of pattern in the real binary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump test | grep 0cc7
000cc70 528b 4830 e789 5c89 1024 c748 2444 0008
008a700 0001 0087 0780 4c98 f000 0cc7 0000 0100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see our value on address &lt;code&gt;0x008a700&lt;/code&gt;. Right now, if we run the binary,&lt;br&gt;
we get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ ./test 
*a = cc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, I used &lt;code&gt;bvi&lt;/code&gt; to quickly find the address and changed &lt;code&gt;0cc7&lt;/code&gt; to &lt;code&gt;0dd7&lt;/code&gt;.&lt;br&gt;
So the result was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump test | grep 0dd7
000dd70 01bb 0000 e900 ff76 ffff 0f66 441f 0000
008a700 0001 0087 0780 4c98 f000 0dd7 0000 0100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well, hopefully, I didn't corrupt the binary. When I tried running it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ ./test 
*a = dd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bingo. We didn't recompile the CUDA source, regenerate the CUBIN, or even touch&lt;br&gt;
the host executable's source code. We changed bytes inside the compiled binary,&lt;br&gt;
and the GPU executed the modified instruction.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;We managed to modify code running on the GPU without recompiling it. More&lt;br&gt;
importantly, we have found what appears to be an instruction field containing&lt;br&gt;
our constant. Given what the kernel does, it is likely part of a &lt;code&gt;mov&lt;/code&gt;-like&lt;br&gt;
instruction, although we haven't decoded it yet.&lt;/p&gt;
&lt;p&gt;So the next question is obvious: what is the real encoding of the instruction?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26347/reverse-engineering-nvidia-modifying-a-cuda-binary</guid>
<pubDate>Sun, 06 Sep 2026 18:41:28 +0000</pubDate>
</item>
</channel>
</rss>