Bitflips in model weights

takis17: <p>Hello everyone,</p>
<p>Hope you are all doing well.<br>
I got interested in investigating random bit-flips on the weights of a model.<br>
I have managed to extract the weights but still haven’t figured out how to induce these bit flips on the weights for a example of a convolutional layer<br>
Looking forward to hear some ideas you might have</p>
<p>Thanks</p>

ptrblck: <p>I don’t know what your exact use case is, but you could use <code>view</code> to reinterpret the <code>float32</code> parameters as <code>int32</code>, manipulate the bits you want, and copy the weight back to the module as seen here:</p>
<pre><code class="lang-python">conv = nn.Conv2d(1, 1, 3, bias=False)
print(conv.weight)
# Parameter containing:
# tensor([[[[-0.2380,  0.0261,  0.2133],
#           [-0.3071, -0.3317,  0.3193],
#           [-0.0558,  0.3280,  0.1207]]]], requires_grad=True)

orig_weight = conv.weight.clone()
# reinterpret as int32
weight_int32 = orig_weight.view(torch.int32)
print(weight_int32)
# tensor([[[[-1099712250,  1020618774,  1046115288],
#           [-1096991409, -1096167765,  1050902816],
#           [-1117487605,  1051193350,  1039613558]]]], dtype=torch.int32)

print(format(weight_int32[0, 0, 0, 1], "b"))
# 111100110101010110100000010110
# corresponds to the float32 number as seen here: https://float.exposed/0x3cd56816

# manipulate bit
bit = 1 &lt;&lt; 24
weight_int32[0, 0, 0, 1] ^= bit
print(format(weight_int32[0, 0, 0, 1], "b"))
# 111101110101010110100000010110
print(weight_int32[0, 0, 0, 1])
# tensor(1037395990, dtype=torch.int32)
weight_float32 = weight_int32.view(torch.float32)
print(weight_float32[0, 0, 0, 1])
# tensor(0.1042) corresponds to https://float.exposed/0x3dd56816

# set manipulated weight to conv layer
with torch.no_grad():
    conv.weight.copy_(weight_float32)
print(conv.weight)
# Parameter containing:
# tensor([[[[-0.2380,  0.1042,  0.2133],
#           [-0.3071, -0.3317,  0.3193],
#           [-0.0558,  0.3280,  0.1207]]]], requires_grad=True)
</code></pre>
<p>Note that bit manipulations in the floating point representation are not trivial to interpret and could easily create huge or tiny numbers, but as I said I don’t know what your use case is.</p>

takis17: <p>I have sampled some weights from the weight_int32, but I want to convert all the sample data to binary form<br>
Is there a quick way of doing that ?</p>

ptrblck: <p>I don’t fully understand the question as my example shows how to manipulate the bits e.g. via the XOR operation at any bit position.</p>

takis17: <p>Hi,</p>
<p>Sorry for not elaborating more.</p>
<pre><code class="lang-auto"># reinterpret as int32
weight_int32 = orig_weight.view(torch.int32)
print(weight_int32)
# tensor([[[[-1099712250,  1020618774,  1046115288],
#           [-1096991409, -1096167765,  1050902816],
#           [-1117487605,  1051193350,  1039613558]]]], dtype=torch.int32)

print(format(weight_int32[0, 0, 0, 1], "b"))
</code></pre>
<p>Here at the print(format(weight_int32[0, 0, 0, 1], “b”)) only the number 1020618774 is converter in binary.<br>
My question is how to convert all the numbers in the tensor to in their binary form</p>

ptrblck: <p>You could iterate all values and manipulate their data separately. In my code snippet I picket a random value to show how the actual manipulation is done.</p>

nkdatascientist: <p>Hi <a class="mention" href="/u/takis17">@takis17</a><br>
I don’t know what your exact use case is, but you can try the Quantization method for changing the layer bitwidth.</p>
<ol>
<li><a href="https://github.com/quic/aimet" rel="noopener nofollow ugc">AIMET</a></li>
<li><a href="https://pytorch.org/docs/stable/quantization.html" rel="noopener nofollow ugc">pytorch</a></li>
</ol>

takis17: <p>Here is a piece of my code <a class="mention" href="/u/ptrblck">@ptrblck</a> <a class="mention" href="/u/nkdatascientist">@nkdatascientist</a></p>
<pre><code class="lang-auto"># sampling
sample_size = int(0.05 * len(weight_int32)) # 5% sample
sample_indices = torch.randperm(len(weight_int32))[:sample_size]
sample = weight_int32[sample_indices]

binary_sample = sample.byte()
</code></pre>
<pre><code class="lang-auto">bitflips = torch.randint_like(binary_sample, low=0, high=2) #bit-flips
corrupted_sample = binary_sample ^ bitflips
</code></pre>
<p>This is what I came up with to modify the sample taken to induce bit flips<br>
Then I just pass it through the model’s weight.</p>
<pre><code class="lang-auto">with torch.no_grad():
    base_model.encoder[0].weight.copy_(weight_float32)
print(base_model.encoder[0].weight)
</code></pre>
<p>Is my methodology correct, or is there any feedback to my code I might need to think about?</p>
<p>Thanks</p>

ptrblck: <p>I don’t think your approach is correct due to a few reasons:</p>
<ul>
<li>
<code>sample_size = int(0.05 * len(weight_int32)) # 5% sample</code> will return <code>0</code> since <code>len(weight_int32)==1</code> as there is only a single <code>weight</code> registered in the layer. I assume you want to check the number of values inside the <code>weight</code> and thus should use something like <code>sample_size = int(0.25 * weight_int32.nelement()) # 5% sample</code>. The same applies for the <code>sample_indices</code> calculation.</li>
<li>
<code>binary_sample = sample.byte()</code> looks wrong too, since you are now downcasting from <code>int32</code> to <code>uint8</code>, which will under-/overflow the values. Example:</li>
</ul>
<pre><code class="lang-python">sample = weight_int32.view(-1)[sample_indices]
# tensor([ 1048365694, -1098724192], dtype=torch.int32)
binary_sample = sample.byte()
# tensor([126, 160], dtype=torch.uint8)
</code></pre>
<ul>
<li>This line <code>bitflips = torch.randint_like(binary_sample, low=0, high=2) #bit-flips</code> will create zeros and ones in the same of <code>binary_sample</code> and will not move the bits to any random position as seen in my code example.</li>
</ul>

takis17: <p>Thank you for the clarification.</p>
<p>Just to provide some more information that I missed, the <code>len(weight_int32)</code> in my case is 64 therefore the 5% of 64 is equal to 3.<br>
Therefore I agree that my method is wrong, the same goes with the sample_indeces.<br>
In my example, the weight are 1728 therefore the sample size of 0.25 of 1728 equals to 432.<br>
When I print the samples <code>sample = weight_int32[sample_indices]</code><br>
I get <code>torch.Size([432, 3, 3, 3])</code></p>
<p>Now this issue is resolved. Thanks</p>
<p>My issue is now trying to convert the individual elements to their binary form<br>
Error:</p>
<pre><code class="lang-auto">RuntimeError: CUDA error: device-side assert triggered
CUDA kernel errors might be asynchronously reported at some other API call,so the stacktrace below might be incorrect.
For debugging consider passing CUDA_LAUNCH_BLOCKING=1.
</code></pre>
<p>This happens, trying, for example, to convert it to a float32 in google colab. (but I think this is an issue with colab)</p>
<p>My purpose it to convert it to their binary form, as you have done in your code as well.<br>
So I can then add random bit flips to the sample data .</p>
<p>Thanks</p>

ptrblck: <p>Rerun the code with <code>export CUDA_LAUNCH_BLOCKING=1</code> and check the stacktrace in the error message, which should point to the failing operation as the error could be raised by another part of the code.</p>

takis17: <p>This event happens when I try to run</p>
<pre><code class="lang-auto">print(weight_int32)
</code></pre>
<p>This was working before, but once I run</p>
<pre><code class="lang-auto">print(sample)
</code></pre>
<p>I get the same error.</p>
<p>I tried <code> export CUDA_LAUNCH_BLOCKING=1</code> but it doesn’t provide me with more information.<br>
I also tried the other solution I found online but still nothing.</p>
<p>Other than that,  what is the correct approach to convert them it to their binary form, as you have done in your code as well.</p>

ptrblck: <p>Are you running your script in the terminal or a notebook? In the latter case, please run it in a terminal an <code>export</code> the env variable before executing the script.</p>
<aside class="quote no-group" data-username="takis17" data-post="13" data-topic="173545">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/t/ba9def/48.png" class="avatar"> takis17:</div>
<blockquote>
<p>Other than that, what is the correct approach to convert them it to their binary form, as you have done in your code as well.</p>
</blockquote>
</aside>
<p>I’m still unsure what the issue is as my code snippet is executable.<br>
Is something not working with it or could you describe which part is unclear?</p>

takis17: <p>Hi,</p>
<p>Yes, your code snippet is executable and was very helpful.<br>
I have managed to loop through the weight_int32 tensor by:</p>
<pre><code class="lang-auto"># Looping through tensor converting values of weights to their binary representations
for i in range(weight_int32.shape[0]):
    for j in range(weight_int32.shape[1]):
        for k in range(weight_int32.shape[2]):
            for l in range(weight_int32.shape[3]):
              print(format(weight_int32[i, j, k, l], "b")) # printing all the values in the tensor to binary
</code></pre>
<p>But I only want to take a random sample of the weight_int32 tensor values and change them to their binary format</p>
<pre><code class="lang-auto"># manipulate bit
bit = 1 &lt;&lt; 24
for i in range(weight_int32.shape[0]):
    for j in range(weight_int32.shape[1]):
        for k in range(weight_int32.shape[2]):
            for l in range(weight_int32.shape[3]):
                weight_int32[i, j, k, l] ^= bit
                print(format(weight_int32[i, j, k, l], "b"))
</code></pre>
<p>Thanks</p>

ptrblck: <p>I assume you mean a random value from the <code>weight</code> tensor by “random sample”?<br>
If so, you can select values randomly via e.g.:</p>
<pre><code class="lang-python">conv = nn.Conv2d(1, 1, 3, bias=False)
random_weight_sample = conv.weight.view(-1)[torch.randperm(conv.weight.nelement())[0]]
</code></pre>
<p>Of course you can also apply the same logic inside the nested loop if needed.</p>

takis17: <p>Hi,</p>
<p>Thank you for the guidance.<br>
Just one more question</p>
<pre><code class="lang-auto">random_weight_sample = weight_int32.view(-1)[torch.randperm(weight_int32.nelement())[:sample_size]]
</code></pre>
<pre><code class="lang-auto">for element in random_weight_sample:
    print(format(element, "b"))
</code></pre>
<p>This loops through the sample and convert them to binary form, but I cannot store them so<br>
I can later make random changes to the bits - converting 0 to 1 and 1 to 0<br>
by XOR <code>bit = random.randint(0, 1)</code><br>
And then following the same logic behind your code snippet at the beginning.</p>

ptrblck: <p>The loop only prints the data in its binary form, the actual type is still <code>int32</code>.<br>
The bit manipulation is performed by shifting a single <code>1</code> bit via:</p>
<pre data-code-wrap="python"><code class="lang-python">bit = 1 &lt;&lt; 24
</code></pre>
<p>which would move it 24 positions to the left and then flipping the bit in the <code>int32</code> via:</p>
<pre data-code-wrap="python"><code class="lang-python">data ^= bit
</code></pre>
<aside class="quote no-group" data-username="takis17" data-post="17" data-topic="173545">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/t/ba9def/48.png" class="avatar"> takis17:</div>
<blockquote>
<p>I can later make random changes to the bits - converting 0 to 1 and 1 to 0<br>
by XOR <code>bit = random.randint(0, 1)</code></p>
</blockquote>
</aside>
<p>No, don’t try to interpret the data as a string of ones and zeros, but use <code>print(format(element, "b"))</code> only to print the bits for debugging. The actual bit manipulation is done in the <code>int32</code> format.</p>