SDXLの量子化2

前回の記事でSDXLの重みをfloat16からInt型に変換して再度float16に戻すことをやった。
前回のコードを少しだけ変えた他の実験をしてみる。

https://qiita.com/nishiha/items/af9ff27e45321ebd7310


# 1. linear(Int)とConv2D(Int)
前回、全結合重み(linear)と畳み込み重み(Conv2D)のそれぞれの割合は86%と13%だったが、畳み込み重みの方が重みの割合のわりに量子化は弱く、全結合重み(linear)のみ量子化した。
それぞれの重みを単純なInt型に変換する時、その変換bit数の組み合わせを確認する。
結果としてはやはり畳み込み重みの方が量子化に弱い。また、Conv2D重みがfloat16からInt8型でも劣化はほとんどない。

```python
for conv_bit in [8,7,6,5,4,3]:
    for linear_bit in [8,7,6,5,4,3]:
        pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
        pipe.enable_model_cpu_offload()
        
        for name, param in pipe.unet.named_parameters():
            if len(param.size())==4:
                print(name, param.size(), type(param))
                M = param.data.clone().to(torch.float32)
                c = (2.0**(conv_bit-1)-1.0) / torch.max(torch.abs(M))
                M2 = (torch.round(M * c)).to(torch.int8)
                M2 = M2.to(torch.float16) / c
                param.data = M2
            
            if len(param.size())==2 and not('emb' in name):
                print(name, param.size(), type(param))
                M = param.data.clone().to(torch.float32)
                c = (2.0**(linear_bit-1)-1.0) / torch.max(torch.abs(M))
                M2 = (torch.round(M * c)).to(torch.int8)
                M2 = M2.to(torch.float16) / c
                param.data = M2

        generator = generator.manual_seed(seed)
        image = pipe(prompt=prompt, generator=generator).images[0]
        image.save(output_path + "img_linear_int%d_conv_int%d.png" % (linear_bit, conv_bit))
```
![concat03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/179a1f04-e6a1-5169-a429-25b9b506b41d.png)

# 2. linear(NF4)とConv2D(Int)
全結合重み(linear)がNF4で畳み込み重み(Conv2D)がInt型に変換した時を試す。
前回、全結合重みがNF4(bit4.5)で畳み込み重みがbit8なら約5.1bitで済むと書いたがその時の結果は示さなかった。

```python
q = [-1.0, -0.6961928009986877, -0.5250730514526367,
     -0.39491748809814453, -0.28444138169288635, -0.18477343022823334,
     -0.09105003625154495, 0.0, 0.07958029955625534, 0.16093020141124725,
      0.24611230194568634, 0.33791524171829224, 0.44070982933044434,
      0.5626170039176941, 0.7229568362236023, 1.0]

q.append(1.01)

for conv_bit in [8,7,6,5,4,3]:
    pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
    pipe.enable_model_cpu_offload()
    
    for name, param in pipe.unet.named_parameters():
        if len(param.size())==4:
            print(name, param.size(), type(param))
            M = param.data.clone().to(torch.float32)
            c = (2.0**(conv_bit-1)-1.0) / torch.max(torch.abs(M))
            M2 = (torch.round(M * c)).to(torch.int8)
            M2 = M2.to(torch.float16) / c
            param.data = M2
        
        if len(param.size())==2 and not('emb' in name):
            print(name, param.size(), type(param))
            M = param.data.clone().to(torch.float32)
            
            block_size = 64
            orig_shape = M.shape
            orig_length = len(M.flatten())
            if orig_length%block_size==0:
                extend_M = M.flatten()
            else:
                extend_M = torch.cat((M.flatten(), torch.zeros(block_size - orig_length%block_size)))
            extend_length = len(extend_M)

            Mmax = []
            for i in range(extend_length//block_size):
                Mmax.append(torch.max(torch.abs(extend_M[block_size*i:block_size*(i+1)])))
            Mmax = torch.stack(Mmax, dim=0)
            Mmax = Mmax.repeat_interleave(block_size)

            M2 = extend_M / Mmax
            for i in range(16):
                M2 = torch.where(M2 <= (q[i]+q[i+1])/2, 100+i, M2)
            M2 = torch.round(M2)

            M2 = M2.to(torch.int8)
            M3 = M2.to(torch.float16)
            for i in range(16):
                M3 = torch.where(i+100==M2, q[i], M3)
            M3 = M3 * Mmax
            M3 = M3[:orig_length].reshape(orig_shape).to(torch.float16)
            param.data = M3

    generator = generator.manual_seed(seed)
    image = pipe(prompt=prompt, generator=generator).images[0]
    image.save(output_path + "img_linear_NF4_conv_int%d.png" % (conv_bit))
```
![concat04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/31c0e7bc-7453-6519-0ee3-1cd1004b2a54.png)

# 3. linear(NF4)の分割数
前回、NF4はこのひとつの結果しか示せなかった。
これはQLoRAの論文に書いてある長さ16のqテーブルが必要で、NF3なら長さ8、NF5なら長さ32のqテーブルが必要である。qの値自体は前回の記事内で考察したのだが、自分が求めたのは対称で、論文中のは非対称であったのでよく分からなかった。
対称であることを気にせず、任意長さの対称のqテーブルを作って変換してみる。なお、分割する数は必ずしも$2^N$である必要はないので、好きな数を取れる。
NF4(original)は非対称で、NF4.00(16)はqテーブルが対称である。
意外に分割数が小さくても馬と宇宙飛行士は描けている。

```python
def make_q(split_num=16):
    x = np.linspace(-1, 1, split_num+1)
    Qx = []
    for i in list(x):
        Qx.append(torch.erfinv(torch.tensor(i/1.0*0.96, dtype=torch.float32)))
    Qx = torch.stack(Qx)
    Qx = Qx.to('cpu').detach().numpy().copy()

    q = np.zeros(split_num)
    for i in range(split_num):
        q[i] = (Qx[i]+Qx[i+1])/2
    q = q/np.max(q)

    q = list(q)
    q.append(1.01)
    return q

for split_num in [32,28,24,20,16,14,12,10,8,7,6]:
    pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
    pipe.enable_model_cpu_offload()
    q = make_q(split_num)
    
    for name, param in pipe.unet.named_parameters():
        if len(param.size())==2 and not('emb' in name):
            print(name, param.size(), type(param))
            M = param.data.clone().to(torch.float32)
            
            block_size = 64
            orig_shape = M.shape
            orig_length = len(M.flatten())
            if orig_length%block_size==0:
                extend_M = M.flatten()
            else:
                extend_M = torch.cat((M.flatten(), torch.zeros(block_size - orig_length%block_size)))
            extend_length = len(extend_M)

            Mmax = []
            for i in range(extend_length//block_size):
                Mmax.append(torch.max(torch.abs(extend_M[block_size*i:block_size*(i+1)])))
            Mmax = torch.stack(Mmax, dim=0)
            Mmax = Mmax.repeat_interleave(block_size)

            M2 = extend_M / Mmax
            for i in range(split_num):
                M2 = torch.where(M2 <= (q[i]+q[i+1])/2, 50+i, M2)
            M2 = torch.round(M2)

            M2 = M2.to(torch.int8)
            M3 = M2.to(torch.float16)
            for i in range(split_num):
                M3 = torch.where(i+50==M2, q[i], M3)
            M3 = M3 * Mmax
            M3 = M3[:orig_length].reshape(orig_shape).to(torch.float16)
            param.data = M3

    generator = generator.manual_seed(seed)
    image = pipe(prompt=prompt, generator=generator).images[0]
    image.save(output_path + "img_NF4(%d).png" % (split_num))
```

![concat05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/c99a1f1e-b3b3-a747-612e-1f005d01a0b4.png)

# 4-1. Int BlockwiseのBlock_size
Int BlockwiseはBlock_size=64の時、変数64個毎に32bitの変換係数$c$を保持する。
これは全結合重みの最大値の平均を小さくし、重みの外れ値に対して強くなるというメリットがある。一方で、ブロック毎にこの$c$を保持する必要があり、これが$32bit/64=0.5bit$になるので余計に一パラメータ当たり0.5bit保持しなければならない。
仮にBlock_size=128なら一パラメータ当たり0.25bit、Block_size=256なら一パラメータ当たり0.125bitで済むのでこの方向で減らせないか考えた。結果から言うのこの方向ではあまり良くない。

```python
for block_size in [32,64,128,256,512,1024]:
    pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
    pipe.enable_model_cpu_offload()
    bit = 4
    for name, param in pipe.unet.named_parameters():
        if len(param.size())==2 and not('emb' in name):
            print(name, param.size(), type(param))
            M = param.data.clone().to(torch.float32)
            
            orig_shape = M.shape
            orig_length = len(M.flatten())
            if orig_length%block_size==0:
                extend_M = M.flatten()
            else:
                extend_M = torch.cat((M.flatten(), torch.zeros(block_size - orig_length%block_size)))
            extend_length = len(extend_M)

            Mmax = []
            for i in range(extend_length//block_size):
                Mmax.append(torch.max(torch.abs(extend_M[block_size*i:block_size*(i+1)])))
            Mmax = torch.stack(Mmax, dim=0)
            Mmax = Mmax.repeat_interleave(block_size)

            c = (2.0**(bit-1)-1.0) / Mmax
            M2 = (torch.round(extend_M * c)).to(torch.int8)
            M2 = M2.to(torch.float16) / c
            M2 = M2[:orig_length].reshape(orig_shape).to(torch.float16)
            param.data = M2

    generator = generator.manual_seed(seed)
    image = pipe(prompt=prompt, generator=generator).images[0]
    image.save(output_path + "img_int4_block_size_%d.png" % (block_size))
```

# 4-2. Double Quantization(DQ)
前述のBlock_sizeを変えた結果はあまり良くなかったので、QLoRAの論文に載っている二重量子化について考えてみる。
いま、二個のblocksizeを考え、それぞれ``block_size1 = 64``と``block_size2 = 64*256``内での最大値Mmax1, Mmax2を取る。この時、Mmax1/Mmax2は正の1～0の数値に必ずなるので、これを符号なしのunsigned int8型に変換し、64個毎に8bitのc1と64*256個毎に32bitのc2から当初の変換係数$c$を``c = c2 / (c1/255.0)``で定義する。
この時、8/64 + 32/(64 · 256) = 0.127bitとなり、Blockwiseによる一パラメータ当たり0.5bit増加よりコストが抑えられる。

```python
pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
pipe.enable_model_cpu_offload()
bit = 4
for name, param in pipe.unet.named_parameters():
    if len(param.size())==2 and not('emb' in name):
        print(name, param.size(), type(param))
        M = param.data.clone().to(torch.float32)
        
        block_size1 = 64
        block_size2 = 64*256
        orig_shape = M.shape
        orig_length = len(M.flatten())
        if orig_length%block_size2==0:
            extend_M = M.flatten()
        else:
            extend_M = torch.cat((M.flatten(), torch.zeros(block_size2 - orig_length%block_size2)))
        extend_length = len(extend_M)

        Mmax1 = []
        for i in range(extend_length//block_size1):
            Mmax1.append(torch.max(torch.abs(extend_M[block_size1*i:block_size1*(i+1)])))
        Mmax1 = torch.stack(Mmax1, dim=0)
        Mmax1 = Mmax1.repeat_interleave(block_size1)

        Mmax2 = []
        for i in range(extend_length//block_size2):
            Mmax2.append(torch.max(torch.abs(extend_M[block_size2*i:block_size2*(i+1)])))
        Mmax2 = torch.stack(Mmax2, dim=0)
        Mmax2 = Mmax2.repeat_interleave(block_size2)
        
        c1 = torch.round(255.0 * Mmax1 / Mmax2).to(torch.uint8)
        c1 = c1.to(torch.float32).clamp(min=1e-12)
        c2 = (2.0**(bit-1)-1.0) / Mmax2

        c = c2 / (c1/255.0)
        M2 = (torch.round(extend_M * c)).to(torch.int8)
        M2 = M2.to(torch.float16) / c
        M2 = M2[:orig_length].reshape(orig_shape).to(torch.float16)
        param.data = M2

generator = generator.manual_seed(seed)
image = pipe(prompt=prompt, generator=generator).images[0]
image.save(output_path + "img_int4_DQ_uint8.png")
```

一応、QLoRAの論文中にはuint8ではなくFP8で保持すると書かれてあるのでその時の変換も考えてみたものの結果は良くない。自分の実装の仕方が何か間違っているかもしれない。

自分の考えだとE6M2の場合、符号bitはなく、1～0.5を6bitで等間隔に刻み指数bit00、0.5～0.25を6bitで等間隔に刻み指数bit01、0.25～0.125を6bitで等間隔に刻み指数bit10、0.125～0.00625を6bitで等間隔に刻み指数bit11、0.00625~0は表現できない。


```python
pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
pipe.enable_model_cpu_offload()
bit = 4
for name, param in pipe.unet.named_parameters():
    if len(param.size())==2 and not('emb' in name):
        print(name, param.size(), type(param))
        M = param.data.clone().to(torch.float32)
        
        block_size1 = 64
        block_size2 = 64*256
        orig_shape = M.shape
        orig_length = len(M.flatten())
        if orig_length%block_size2==0:
            extend_M = M.flatten()
        else:
            extend_M = torch.cat((M.flatten(), torch.zeros(block_size2 - orig_length%block_size2)))
        extend_length = len(extend_M)

        Mmax1 = []
        for i in range(extend_length//block_size1):
            Mmax1.append(torch.max(torch.abs(extend_M[block_size1*i:block_size1*(i+1)])))
        Mmax1 = torch.stack(Mmax1, dim=0)
        Mmax1 = Mmax1.repeat_interleave(block_size1)

        Mmax2 = []
        for i in range(extend_length//block_size2):
            Mmax2.append(torch.max(torch.abs(extend_M[block_size2*i:block_size2*(i+1)])))
        Mmax2 = torch.stack(Mmax2, dim=0)
        Mmax2 = Mmax2.repeat_interleave(block_size2)
        
        Mmax = Mmax1 / Mmax2
        c0 = torch.zeros_like(Mmax) # E6M2
        for i in range(4):
            c0 = torch.where(Mmax <= 1/(2.0**i), i, c0)
        c0 = torch.round(c0).to(torch.uint8).to(torch.float32)
        
        c1 = torch.round(63.0 * 2.0 * (Mmax1 / Mmax2 * (2.0**c0)) - 1.0).to(torch.uint8)
        c1 = c1.to(torch.float32)
        c2 = (2.0**(bit-1)-1.0) / Mmax2

        c = c2 / (((c1/63.0)/2.0 + 0.5) / (2.0**c0)).clamp(min=1e-12)
        M2 = (torch.round(extend_M * c)).to(torch.int8)
        M2 = M2.to(torch.float16) / c
        M2 = M2[:orig_length].reshape(orig_shape).to(torch.float16)
        param.data = M2

generator = generator.manual_seed(seed)
image = pipe(prompt=prompt, generator=generator).images[0]
image.save(output_path + "img_int4_DQ_FP8_E6M2.png")


pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
pipe.enable_model_cpu_offload()
bit = 4
for name, param in pipe.unet.named_parameters():
    if len(param.size())==2 and not('emb' in name):
        print(name, param.size(), type(param))
        M = param.data.clone().to(torch.float32)
        
        block_size1 = 64
        block_size2 = 64*256
        orig_shape = M.shape
        orig_length = len(M.flatten())
        if orig_length%block_size2==0:
            extend_M = M.flatten()
        else:
            extend_M = torch.cat((M.flatten(), torch.zeros(block_size2 - orig_length%block_size2)))
        extend_length = len(extend_M)

        Mmax1 = []
        for i in range(extend_length//block_size1):
            Mmax1.append(torch.max(torch.abs(extend_M[block_size1*i:block_size1*(i+1)])))
        Mmax1 = torch.stack(Mmax1, dim=0)
        Mmax1 = Mmax1.repeat_interleave(block_size1)

        Mmax2 = []
        for i in range(extend_length//block_size2):
            Mmax2.append(torch.max(torch.abs(extend_M[block_size2*i:block_size2*(i+1)])))
        Mmax2 = torch.stack(Mmax2, dim=0)
        Mmax2 = Mmax2.repeat_interleave(block_size2)
        
        Mmax = Mmax1 / Mmax2
        c0 = torch.zeros_like(Mmax) # E5M3
        for i in range(8):
            c0 = torch.where(Mmax <= 1/(2.0**i), i, c0)
        c0 = torch.round(c0).to(torch.uint8).to(torch.float32)
        
        c1 = torch.round(31.0 * 2.0 * (Mmax1 / Mmax2 * (2.0**c0)) - 1.0).to(torch.uint8)
        c1 = c1.to(torch.float32)
        c2 = (2.0**(bit-1)-1.0) / Mmax2

        c = c2 / (((c1/31.0)/2.0 + 0.5) / (2.0**c0)).clamp(min=1e-12)
        M2 = (torch.round(extend_M * c)).to(torch.int8)
        M2 = M2.to(torch.float16) / c
        M2 = M2[:orig_length].reshape(orig_shape).to(torch.float16)
        param.data = M2

generator = generator.manual_seed(seed)
image = pipe(prompt=prompt, generator=generator).images[0]
image.save(output_path + "img_int4_DQ_FP8_E5M3.png")
```
![concat06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/aeee6083-15bd-5384-aab1-ab783d5c7946.png)

# 5. Linear(NF2.81(7)DQ),Conv(NF5(32)DQ),Bias(Int8)
上述の色々な検討を全部乗せてみる。
この時の一パラメータ当たりの容量は理論上(1/100\*8/16+13/100\*(5+0.127)/16+86/100\*(2.81+0.127)/16)\*16=3.27bitである。ただし、現行NF2.81(7)とかを効率よく保存できる方法があるわけではない。

```python
pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
pipe.enable_model_cpu_offload()
split_num1 = 7
split_num2 = 32
q1 = make_q(split_num1)
q2 = make_q(split_num2)

for name, param in pipe.unet.named_parameters():
    if len(param.size())==1:
        print(name, param.size(), type(param))
        M = param.data.clone().to(torch.float32)
        c = 127.0 / torch.max(torch.abs(M))
        M2 = (torch.round(M * c)).to(torch.int8)
        M2 = M2.to(torch.float16) / c
        param.data = M2.to(torch.float16)

    if len(param.size())==4:
        print(name, param.size(), type(param))
        M = param.data.clone().to(torch.float32)
        
        block_size1 = 64
        block_size2 = 64*256
        orig_shape = M.shape
        orig_length = len(M.flatten())
        if orig_length%block_size2==0:
            extend_M = M.flatten()
        else:
            extend_M = torch.cat((M.flatten(), torch.zeros(block_size2 - orig_length%block_size2)))
        extend_length = len(extend_M)

        Mmax1 = []
        for i in range(extend_length//block_size1):
            Mmax1.append(torch.max(torch.abs(extend_M[block_size1*i:block_size1*(i+1)])))
        Mmax1 = torch.stack(Mmax1, dim=0)
        Mmax1 = Mmax1.repeat_interleave(block_size1)

        Mmax2 = []
        for i in range(extend_length//block_size2):
            Mmax2.append(torch.max(torch.abs(extend_M[block_size2*i:block_size2*(i+1)])))
        Mmax2 = torch.stack(Mmax2, dim=0)
        Mmax2 = Mmax2.repeat_interleave(block_size2)
        
        c1 = torch.round(255.0 * Mmax1 / Mmax2).to(torch.uint8)
        c1 = c1.to(torch.float32).clamp(min=1e-12)
        
        M2 = extend_M / (Mmax2 * c1 / 255.0)
        for i in range(split_num2):
            M2 = torch.where(M2 <= (q2[i]+q2[i+1])/2, 50+i, M2)
        M2 = torch.round(M2)

        M2 = M2.to(torch.int8)
        M3 = M2.to(torch.float16)
        for i in range(split_num2):
            M3 = torch.where(i+50==M2, q2[i], M3)
        M3 = M3 * (Mmax2 * c1 / 255.0)
        M3 = M3[:orig_length].reshape(orig_shape).to(torch.float16)
        param.data = M3

    if len(param.size())==2 and not('emb' in name):
        print(name, param.size(), type(param))
        M = param.data.clone().to(torch.float32)
        
        block_size1 = 64
        block_size2 = 64*256
        orig_shape = M.shape
        orig_length = len(M.flatten())
        if orig_length%block_size2==0:
            extend_M = M.flatten()
        else:
            extend_M = torch.cat((M.flatten(), torch.zeros(block_size2 - orig_length%block_size2)))
        extend_length = len(extend_M)

        Mmax1 = []
        for i in range(extend_length//block_size1):
            Mmax1.append(torch.max(torch.abs(extend_M[block_size1*i:block_size1*(i+1)])))
        Mmax1 = torch.stack(Mmax1, dim=0)
        Mmax1 = Mmax1.repeat_interleave(block_size1)

        Mmax2 = []
        for i in range(extend_length//block_size2):
            Mmax2.append(torch.max(torch.abs(extend_M[block_size2*i:block_size2*(i+1)])))
        Mmax2 = torch.stack(Mmax2, dim=0)
        Mmax2 = Mmax2.repeat_interleave(block_size2)
        
        c1 = torch.round(255.0 * Mmax1 / Mmax2).to(torch.uint8)
        c1 = c1.to(torch.float32).clamp(min=1e-12)
        
        M2 = extend_M / (Mmax2 * c1 / 255.0)
        for i in range(split_num1):
            M2 = torch.where(M2 <= (q1[i]+q1[i+1])/2, 50+i, M2)
        M2 = torch.round(M2)

        M2 = M2.to(torch.int8)
        M3 = M2.to(torch.float16)
        for i in range(split_num1):
            M3 = torch.where(i+50==M2, q1[i], M3)
        M3 = M3 * (Mmax2 * c1 / 255.0)
        M3 = M3[:orig_length].reshape(orig_shape).to(torch.float16)
        param.data = M3

generator = generator.manual_seed(seed)
image = pipe(prompt=prompt, generator=generator).images[0]
image.save(output_path + "img_linear_NF4(7)DQ_Conv_NF4(32)DQ_bias_Int8.png")
```
![concat07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/2599f336-d3ea-039a-ae41-9c83c1fe980f.png)

# まとめ
前回記事では、全結合重みがNF4(bit4.5)で畳み込み重みがbit8なら約5.1bitという道筋までしか示せなかったが、更なる削減の検討をしてみた。
・全結合(linear)と畳み込み(Conv2D)の重みを異なるbitで量子化する。
・任意長さのqテーブルを作り、NF4(16)以外のNF型を考える。
・DQの実装を試みてblockwiseに必要な追加のbitを0.5bitから0.127bitにした。
この時、4bitを切る容量でも比較的劣化は小さかった。

# その他
Block内最大値の値に依存するqテーブル(仮に最大値が小さければ一様分布に近いと見なし、等間隔に近いqテーブルで分割する)も考えてはみたものの、この検討は自分のコードが悪いのか上手く行かなかった。

これは適当な正規分布乱数をNF4_blockwiseで分割した時(M4)、bitの使用頻度は一見一様分布に近く有効に使えているように見えるが、実際には分布のBlock内の最大値に依存しており、Block内の分布を見ると最大値の大きい値では凸型で、最大値の小さい値では凹型になるように変換されるように見える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/5deddd17-a318-a41c-5c24-6d3cf17da28e.png)

# 追記：NF3.58(12)の圧縮について
0～11の量子化されたint型の数値をint8型より小さい形で効率よく保持する方法はよく分かってなかったが、byte型に変換してbz2で圧縮するのが楽かもしれない。
M2を50～61で量子化されたとして、これに47を足してa~lの文字列にしてからbz2で圧縮してから、元に戻す。asciiコードの"a"が97だから47を足しているが、特に意味はない。
この時、ちゃんと元の値に戻っているのが確認でき、圧縮率も理論的割合に近い。

```python
        split_num = 12
        M2 = M2.to(torch.int8)

        import bz2
        data = (M2+47).to('cpu').detach().numpy().copy().tobytes().decode("ascii").encode("utf-8")

        data2 = bz2.compress(data)
        data3 = bz2.decompress(data2)
        M3 = torch.from_numpy(np.array([data3]).view(np.int8)-47).clone()
        
        print('M2=', M2)
        print('data=', data[:100])
        print('data2=', data2[:100])
        print('data3=', data3[:100])
        print('M3=', M3)
        print('before compress: ', len(data), ', after compress: ', len(data2))
        print('real compress rate: ', len(data2)/len(data))
        print('ideal compress rate: ', np.log(split_num)/np.log(2) / 8.0)
----------------------------------------------------------------
M2= tensor([53, 53, 55,  ..., 55, 52, 53], dtype=torch.int8)
data= b'ddfiidgdhigjgbjchdbgggdecccijdjfcihdbehefdhdjfciegkhcggeigakjcaefkfahbhechedgbeddbhgfekkjiihfhfgjkih'
data2= b'BZh91AY&SY"HY\xf7\x00\xa5PI\x00x\x00@\x00?\xfcc\xf6\x1fx\x12\xb6\xed\xbb\xdb\xd3\xed\xf5\xdfs}{\xc7\x98\xe5+\xb0}\xc3\xd3\x87\xb5;w+}\xb7\xaf9 HM\xdd\xcat\xc9\xabm\xeeu\xeb\xef\xa9\xf7\xdb\xdd\xdf^=\xdc\xbd\xbd\xed\xa7F\x9d\xf6wm\x95\xa5H\xfa\xf9wl\xba\xf7G\xb2\xef'
data3= b'ddfiidgdhigjgbjchdbgggdecccijdjfcihdbehefdhdjfciegkhcggeigakjcaefkfahbhechedgbeddbhgfekkjiihfhfgjkih'
M3= tensor([53, 53, 55,  ..., 55, 52, 53], dtype=torch.int8)
before compress:  409600 , after compress:  187003
real compress rate:  0.45655029296875
ideal compress rate:  0.44812031259014456
```