BiLLMについて

前回BitNetなどに出てくる1bit量子化をSDXLでやったが単なる重み変換だけでは言うほど大した精度は出なかった。少なくとも単なる重み変換だけではなくQATが重要なのだろう。

BiLLMの論文では1.08bitで変換できると言っている。

https://arxiv.org/pdf/2402.04291.pdf

# 前提：残差重みに関して
1bit量子化における差分$\Delta W$を考える。
```math
\alpha = mean(W)
```
```math
\beta = mean(abs(W-\alpha))
```
```math
quantW=\beta * sign(W-\alpha)
```
```math
\Delta W=W-quantW
```

この$\Delta W, (\Delta W)^2,upper\Delta W(when...abs(W)>\beta), lower\Delta W(when...abs(W)<\beta)$を適当な乱数で表示させてみる。

```python
import numpy as np
import matplotlib.pyplot as plt

W = np.random.randn(360, 768)
alpha = np.mean(W)
beta = np.mean(np.abs(W-alpha))
qW = np.sign(W-alpha)
delta = (W - alpha) - qW * beta
delta2 = delta * delta
u_pos = (np.sign(delta * qW)+1.0) / 2.0
u_delta = delta * u_pos
l_pos = (np.sign(-delta * qW)+1.0) / 2.0
l_delta = delta * l_pos
```
この図で得られる知見は$\beta$より大きい場所(upper)の差分と小さい場所(lower)の差分の分布は異なるという事である。小さい場所では一様分布に近く$[-0.798,0.798]$の範囲に過ぎない。一方、大きい場所では一様分布とは言えず、範囲も広く見える$[-2,2]$ので、異なる分布で量子化の係数は異なる。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/6a17127b-5622-a87c-8ead-3f73cf9f98df.png)

# 要素

ある領域以上を$Salient Weight$と呼び、この領域では1bit量子化を2回行う。(1bit量子化を1回行い、残差重みを再1bit量子化する。都合2bit量子化ともいえる)
一方、$NonSalient Weight$ではp以上かp以下で分け、更に1bit量子化を1回のみ行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/503eea7c-2169-0ee9-f889-1fe79b331497.png)

仮に$Salient Weight$を2bit量子化、$Nonsalient Weight$を1bit量子化とすれば、平均の量子化bitはその割合で決まる。割合が$ratio=0.1$なら$0.1\*2bit +0.9\*1bit =1.1bit$である。
また、$Salient Weight$か$Nonsalient Weight$かというフラグ以外に、この重みとは別に$Concentrated Area$か$Sparse Area$かという1bitのフラグを持つ必要がある。結局2bit持つ。
前者の$Salient Weight$か$Nonsalient Weight$かというフラグはLLM等の実際の重みでは実質group化出来る。後者の$Concentrated Area$か$Sparse Area$かというbitは省略できない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/59bc1a55-75b4-52f5-afc4-cee1e2681cb5.png)

論文の式を追うと以下がBinary Residual ApproximationでSalient Weightを二回量子化する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/837cb8ec-21c1-2429-e9df-9f6d748f2efa.png)

一方、次に$Concentrated Area$か$Sparse Area$に量子化する際、p以下とp以上で分け、それぞれを1bitで量子化する。この最適なpを探索する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/982c372e-2f04-df7a-e6a8-483c1434cb31.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/1e292ec3-eb93-50ed-9a40-6e0664ed00cd.png)

まとめると以下の三つの重みを保持する。
## ①Salient Weight
group化したヘッセ行列の大きい順で全体の5%～10%くらい。
Binary Residual Approximationとあるように差分重みを再度量子化するので2bit量子化に近い。
```math
Salient Weight=\alpha_0B_0+\alpha_rB_r(whereH>top_{5\%})
```

## ②Sparse Area Weight
Salient Weightを除いたNon-Salient Weight内で絶対値p**以上**の領域を1bit量子化する。
```math
Sparse Area Weight=\alpha_sB_s(whereH<top_{5\%},abs(W)>p)
```

## ③Concentrated Area Weight
Salient Weightを除いたNon-Salient Weight内で絶対値p**以下**の領域を1bit量子化する。
```math
Concentrated Area Weight=\alpha_cB_c(whereH<top_{5\%},abs(W)<p)
```

## ④flag weights 
Salient WeightとNon-Salient Weightの領域を示すフラグはグループ化したbitなのでblock_size分の1になるから大部分無視できる。Concentrated AreaとSparse Areaを分ける1bitのフラグは省略や無視は出来ないように思う。

```math
Salientflag=topk(abs(s))
```
```math
Concentrated,Sparseflag=where(abs(W)>p, 1.0, 0.0)
```
何故論文中でこの後者の1bitのフラグを量子化重みに計上してないのか謎である。
論文中では``It is important to note that flag weights do not participate in the computation; actual calculations are executed solely with parameter weights. （フラグウェイトは計算に関与せず、実際の計算はパラメータウェイトのみで実行されることに注意することが重要である。）``とあるが、何故省略可能かという理屈に納得できていない。

# 簡易的なテスト
**ヘッセ行列とか残差再量子化とかフラグのグループ化**は複雑なので単純な領域分割して量子化のテストを行うコードを考えたい。
適当な正規分布重みを仮定した時、$\beta=0.798\sigma$として簡易的に$2\sigma$より大きい値(正規分布なら約5%)をSalient Weight、小さい値をNonSalient Weightとしてみる。
次に適当なpを定義し、絶対値がpより大きいのをsparseエリア、pより小さいのをconcentratedエリアとする。
この内、Salient Weightは2bit量子化し、sparse Weight、concentrated Weightは各領域で1bit量子化を行う。
①SalientWeight=2σ<abs(W)
②SparseWeight=pσ<abs(W)<2σ
③ConcentratedWeight=abs(W)<pσ
としてpの値を0.1～1.9まで変えてみる。その場合のcos類似度を比較する。
この手法だと生成される量子化重みは8分割であるから3bit重みに相当する。(正確には3.05bit)


次の左図の折れ線は分割数$s$の場合の以下の量子化の結果である。
```math
\tilde{W} = clip(round((W-alpha)/beta+(s-1)/2), 0.0, (s-1))-(s-1)/2
```
一方、右図の折れ線はSparseWeightとConcentratedWeightをpで分割した場合の性能である。今回のBiLLMの量子化性能は3bit相当である分割数8の場合の性能はよりも低い。
p=0.7～0.9ぐらいで良く、厳密度はあまりない。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/46a90153-decf-d174-d0eb-709e3211ad76.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/656efcd3-d665-740a-21dc-b345b4a477f7.png)


```python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib

epsilon = 10e-9

def part_mean(weight, pos):
    alpha = np.sum(weight) / np.sum(pos)
    beta = np.sum(np.abs(weight - alpha) * pos) / np.sum(pos)
    return alpha, beta

def quant1(W, split_num=4):
    split_num = float(split_num)
    alpha = np.mean(W)
    beta = np.mean(np.abs(W-alpha))
    tilde_W = np.clip(np.round((split_num-1.0)/2.0 + (W-alpha)/(beta+epsilon)), 0.0, (split_num-1.0))
    tilde_W -= (split_num-1.0)/2.0
    if split_num==2.0:
        beta *= 2.0
    return tilde_W * beta

def quant2(W, p=1.0, plot=False):
    alpha = np.mean(W)
    beta = np.mean(np.abs(W-alpha))
    sigma = beta/0.798
    qW = np.sign(W-alpha)
    delta = (W - alpha) - qW * (2 * sigma)
    salient_pos = (np.sign(delta * qW)+1.0) / 2.0
    salient_weight = delta * salient_pos
    nonsalient_pos = 1.0 - salient_pos
    nonsalient_weight = W * nonsalient_pos
    delta2 = (W - alpha) - qW * (p * sigma)
    sparse_pos = (np.sign(delta2 * qW)+1.0) / 2.0 * nonsalient_pos
    sparse_weight = delta2 * sparse_pos
    concentrated_pos = nonsalient_pos - sparse_pos
    concentrated_weight = W * concentrated_pos
    
    salient_alpha, salient_beta = part_mean(salient_weight, salient_pos)
    salient_qW = np.clip(np.round((salient_weight-salient_alpha)/salient_beta + 1.5), 0.0, 3.0) - 1.5
    salient_qW *= salient_pos * salient_beta
    
    sparse_alpha, sparse_beta = part_mean(sparse_weight, sparse_pos)
    sparse_qW = np.sign(sparse_weight-sparse_alpha)
    sparse_qW *= sparse_pos * sparse_beta
    
    concentrated_alpha, concentrated_beta = part_mean(concentrated_weight, concentrated_pos)
    concentrated_qW = np.sign(concentrated_weight-concentrated_alpha)
    concentrated_qW *= concentrated_pos * concentrated_beta
    
    total_qW = salient_qW + sparse_qW + concentrated_qW
    total_qW += ((2 * sigma) * np.sign(salient_qW) * salient_pos)
    total_qW += ((p * sigma) * np.sign(sparse_qW) * sparse_pos)
    
    if plot:
        fig = plt.figure()
        ax1 = fig.add_subplot(2, 3, 1)
        ax2 = fig.add_subplot(2, 3, 2)
        ax3 = fig.add_subplot(2, 3, 3)
        ax4 = fig.add_subplot(2, 3, 4)
        ax5 = fig.add_subplot(2, 3, 5)
        ax6 = fig.add_subplot(2, 3, 6)
        ax1.set_ylim(0, 20000)
        ax1.set_title('W')
        ax1.hist(W.flatten(), bins=64)
        ax2.set_ylim(0, 5000)
        ax2.set_title('salient_weight')
        ax2.hist(salient_weight.flatten(), bins=64)
        ax3.set_ylim(0, 10000)
        ax3.set_title('Nonsalient_weight')
        ax3.hist(nonsalient_weight.flatten(), bins=64)
        ax4.set_ylim(0, 10000)
        ax4.set_title(r'sparse_weight')
        ax4.hist(sparse_weight.flatten(), bins=64)
        ax5.set_ylim(0, 10000)
        ax5.set_title(r'concentrated_weight')
        ax5.hist(concentrated_weight.flatten(), bins=64)
        ax6.set_title(r'total_quantW')
        ax6.hist(total_qW.flatten(), bins=64)
        plt.show()

    return total_qW

def LN(x):
    xmean = np.mean(x)
    xstd  = np.std(x)
    zscore = (x-xmean)/(xstd+epsilon)
    return zscore
def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

x = np.random.randn(768)
W = np.random.randn(360, 768)

x = LN(x)
y = W @ x

for i in range(2,9):
    y1 = quant1(W, i) @ x
    print('cos_sim_%d_01=%f' % (i, cos_sim(y,y1)))

print()
for p in np.linspace(0.1, 1.9, 200):
    y2 = quant2(W, p) @ x
    print('cos_sim_%f=%f' % (p, cos_sim(y,y2)))

y2 = quant2(W, 0.798, True) @ x
```

salient_posはグループ化する事によって大きさをほぼ無視できるからこの実装が入れば3bit中の1bitを無視し、2.1bitに減る。一方、ここから更にsparse_posを量子化bitから無視できる理屈は分からない。x方向に定数であれば確かに演算は無視できるが多分そうでもない。



# SDXLにおけるBiLLM量子化
``to_k``,``to_v``,``to_q``,``to_out``,``ff``の重み全部を量子化する。``proj``,``conv``,``emb``はしない。
group化しないBiLLMの量子化は3.1bit量子化に近い。
この内訳はsalient_pos=1bit、sparse_pos=1bit、salient_weight=2bit\*0.05、nonsalient_weight=1bit\*0.95である。salient_pos+sparse_posを[0,1,2]の三値で持つと考える場合は2.6bitである。
salient_posをgroup化した場合、$\frac{1}{b_{size}}$になるからほぼ1bit減らせ、2.1bit量子化に近くなる。
下記のようにsalient_posのgroup化を実行したが、Bitlinearの4分割(bit2量子化相当)よりは確かにマシなのだが、劣化は無視できない。
![concat07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/84f3bc99-8be3-3469-0d4e-32ad074b668a.png)

これだけだと本当に良くなっているのか分かりにくいので``to_k``,``to_v``,``to_q``,``to_out``,``ff``の各重みに対して適用した結果が以下である。やはり若干改善しているようには見える。
特に``to_q``においては2.1bit量子化でも良く再現出来ている。
![concat10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/9f5b4b65-1ad1-6a85-d7f0-ec4447cc428a.png)

salient重みの基準やpの値を少し変えた結果も示すが、傾向はそれ程変わらなく見える。
![concat09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/37cf8f7d-430c-4ae5-4354-56d4598fdabc.png)


```python
from diffusers import DiffusionPipeline
import numpy as np
import torch

def part_mean(weight, pos):
    alpha = torch.sum(weight) / torch.sum(pos)
    beta = torch.sum(torch.abs(weight - alpha) * pos) / torch.sum(pos)
    return alpha, beta

def BiLLM_quant(param, salient_p = 2.0, p = 0.8, group=False):
    if group:
        orig_shape = param.shape
        if orig_shape[1]%20==0:
            block_size = 20
        else:
            block_size = 16
        param2 = torch.reshape(param, (orig_shape[0], block_size, orig_shape[1]//block_size))
        param2 = torch.mean(param2, dim=1, keepdim=True)
        #param2 = torch.reshape(param2.repeat(1, block_size, 1), orig_shape)
    else:
        param2 = param

    W = param
    W2 = param2

    alpha2 = torch.mean(W2)
    beta2 = torch.mean(torch.abs(W2-alpha2))
    sigma2 = beta2/0.798
    qW2 = torch.sign(W2-alpha2)
    delta2 = (W2 - alpha2) - qW2 * (salient_p * sigma2)
    salient_pos = (torch.sign(delta2 * qW2)+1.0) / 2.0
    
    if group:
        W2 = torch.reshape(W2.repeat(1, block_size, 1), orig_shape)
        delta2 = torch.reshape(delta2.repeat(1, block_size, 1), orig_shape)
        salient_pos = torch.reshape(salient_pos.repeat(1, block_size, 1), orig_shape)
        delta2 += W - W2
    salient_weight = delta2 * salient_pos
    nonsalient_pos = 1.0 - salient_pos

    W = W * nonsalient_pos
    alpha, beta = part_mean(W, nonsalient_pos)
    sigma = beta/0.798
    qW = torch.sign(W-alpha)
    delta = (W - alpha) - qW * (p * sigma)
    sparse_pos = (torch.sign(delta * qW)+1.0) / 2.0 * nonsalient_pos
    sparse_weight = delta * sparse_pos
    concentrated_pos = nonsalient_pos - sparse_pos
    concentrated_weight = W * concentrated_pos
    
    salient_alpha, salient_beta = part_mean(salient_weight, salient_pos)
    salient_qW = torch.clamp(torch.round((salient_weight-salient_alpha)/salient_beta + 1.5), 0.0, 3.0) - 1.5
    salient_qW *= salient_pos * salient_beta
    
    sparse_alpha, sparse_beta = part_mean(sparse_weight, sparse_pos)
    sparse_qW = torch.sign(sparse_weight-sparse_alpha)
    sparse_qW *= sparse_pos * sparse_beta
    
    concentrated_alpha, concentrated_beta = part_mean(concentrated_weight, concentrated_pos)
    concentrated_qW = torch.sign(concentrated_weight-concentrated_alpha)
    concentrated_qW *= concentrated_pos * concentrated_beta
    
    total_qW = salient_qW + sparse_qW + concentrated_qW 
    total_qW += ((salient_p * sigma2) * torch.sign(salient_qW) * salient_pos)
    total_qW += ((p * sigma) * torch.sign(sparse_qW) * sparse_pos)

    return total_qW

def Bitlinear_quant(M, split_num):
    split_num = float(split_num)
    alpha = torch.mean(M)
    beta = torch.mean(torch.abs(M-alpha))
    if split_num==2.0:
        beta *= 2.0
    M = (torch.clip(torch.round((M-alpha) / (beta+epsilon) + (split_num-1.0)/2.0), 0.0, split_num-1.0)).to(torch.uint8)
    M2 = (M.to(torch.float32)-(split_num-1.0)/2.0) * beta
    return M2

model_id = './stable-diffusion-xl-base-1.0/'
output_path = './sdxl_billm/'

prompt = "a photo of an astronaut riding a horse on mars"
seed = 42
generator = torch.Generator(device="cuda")
generator = generator.manual_seed(seed)

salient_p = 2.0
p = 0.8

pipe = DiffusionPipeline.from_pretrained(model_id, use_safetensors=True, torch_dtype=torch.float16, variant="fp16").to("cuda")
pipe.enable_model_cpu_offload()
torch.cuda.empty_cache()

for name, param in pipe.unet.named_parameters():
    orig_data_type = param.data.dtype
    M = param.data.clone().to(torch.float32)
    if len(param.size())==2 and ('to_k' in name): # to_k
        M2 = BiLLM_quant(M, salient_p = salient_p, p = p, group=True)
        #M2 = Bitlinear_quant(M, split_num=4.0)
    elif len(param.size())==2 and ('to_v' in name): # to_v
        M2 = BiLLM_quant(M, salient_p = salient_p, p = p, group=True)
        #M2 = Bitlinear_quant(M, split_num=4.0)
    elif len(param.size())==2 and ('to_q' in name): # to_q
        M2 = BiLLM_quant(M, salient_p = salient_p, p = p, group=True)
        #M2 = Bitlinear_quant(M, split_num=4.0)
    elif len(param.size())==2 and ('to_out' in name): # to_out
        M2 = BiLLM_quant(M, salient_p = salient_p, p = p, group=True)
        #M2 = Bitlinear_quant(M, split_num=4.0)
    elif len(param.size())==2 and ('ff' in name) and not('proj' in name): # ff
        M2 = BiLLM_quant(M, salient_p = salient_p, p = p, group=True)
        #M2 = Bitlinear_quant(M, split_num=4.0)
    else:
        M2 = M

    param.data = M2.to(orig_data_type)
    del M, M2

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

# 参考：
入力次元ではなく出力次元においてgroup化もしてみるがあまり変わらない。
![concat11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/6014d681-fc4a-7074-ed9c-0e23cc5eba49.png)

group化した場合とgroup化しなかった場合の実際の重み分布も示してみてる。
group化する事で$\beta_{group}=mean(abs(W_{group}))< mean(abs(W))$となるため良くないのかもしれない。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/dd07bc35-96d3-0355-4a51-6a1ab9c7a83a.png)
![Figure_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/8edacc77-1217-c0f7-c111-b956db53560f.png)

SDXLの1bit量子化の残差重み$\Delta W$をプロットしてみるが特に何か言える知見はない。
![Figure_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/2ff26287-36ef-1d66-739e-94d6a858d5bb.png)



salient、sparse、concentratedの分割数を変えた時の変化、sparseの分割数を奇数にした時、却って悪化している。p=0.8ではconcentratedの分割数が足りてないように見える。
![concat12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/ae46caf9-4fd1-9bfd-2113-a70476909227.png)

## 出現頻度と圧縮効率
今、salient_posの1bitは全体の僅か5%しか1にならず他は0に過ぎない。論文中はgroup化によってsalient_posを(groupサイズ分の1)にして1bitを削ってる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/017b9630-c4e3-4356-a8dd-9f32832c6d12.png)

実のところ出現頻度が低い情報の場合、仮にsalient_posをgroup化をしなくても圧縮の機構により頻度が小さい分割は1bitより小さい値でこの情報を保持できる。単純なハフマン符号化はどう頑張っても1byteあたりを1bit以下には圧縮出来ないが、LZ77符号化などの辞書法なら出現確率によっては1bit以下にも符号化できるらしい。
参考：

http://www.nct9.ne.jp/m_hiroi/light/pyalgo36.html

https://www.plan-b.co.jp/blog/tech/10282/

lzmaとbz2の圧縮率を見たが出現頻度によってどちらが良いか異なる。gzipはメリットなかった。出現頻度5%程度の1bit重みならbz2では0.32bitに圧縮出来る。（エントロピー上は$-0.05\*log2(0.05)-0.95\*log2(0.95)=0.286bit$）
従って今回のBiLLM重みならgroup化をしなくても量子化重みに1.05bit、フラグ重みに1.32bitで合計2.4bit程度で保持できる筈である。


```python
import numpy as np
import bz2
import gzip
import lzma

text = ''
text2 = ''
text3 = ''
for i in range(1024*1024):
    random_num = np.random.rand()
    random_num2 = np.random.rand()
    if random_num < 0.5:
        text = text + 'a'
    else:
        text = text + 'b'


    if random_num < 0.05:
        text2 = text2 + 'a'
    else:
        text2 = text2 + 'b'


    if random_num < 0.05:
        if random_num2 < 0.25:
            text3 = text3 + 'a'
        elif random_num2 < 0.50:
            text3 = text3 + 'b'
        elif random_num2 < 0.75:
            text3 = text3 + 'c'
        else:
            text3 = text3 + 'd'
    elif random_num < 0.5:
        if random_num2 < 0.5:
            text3 = text3 + 'e'
        else:
            text3 = text3 + 'f'
    else:
        if random_num2 < 0.5:
            text3 = text3 + 'g'
        else:
            text3 = text3 + 'h'

text = text.encode("utf-8")
text2 = text2.encode("utf-8")
text3 = text3.encode("utf-8")

text_compress = bz2.compress(text)
text2_compress = bz2.compress(text2)
text3_compress = bz2.compress(text3)

print('text_compress_bit_bz2=', len(text_compress)/len(text)*8)
print('text2_compress_bit_bz2=', len(text2_compress)/len(text2)*8)
print('text3_compress_bit_bz2=', len(text3_compress)/len(text3)*8)

text_compress = gzip.compress(text)
text2_compress = gzip.compress(text2)
text3_compress = gzip.compress(text3)

print('text_compress_bit_gzip=', len(text_compress)/len(text)*8)
print('text2_compress_bit_gzip=', len(text2_compress)/len(text2)*8)
print('text3_compress_bit_gzip=', len(text3_compress)/len(text3)*8)

text_compress = lzma.compress(text)
text2_compress = lzma.compress(text2)
text3_compress = lzma.compress(text3)

print('text_compress_bit_lzma=', len(text_compress)/len(text)*8)
print('text2_compress_bit_lzma=', len(text2_compress)/len(text2)*8)
print('text3_compress_bit_lzma=', len(text3_compress)/len(text3)*8)
----------------------------------------------------------
text_compress_bit_bz2= 1.2810211181640625
text2_compress_bit_bz2= 0.32028961181640625
text3_compress_bit_bz2= 2.608184814453125
text_compress_bit_gzip= 1.1989059448242188
text2_compress_bit_gzip= 0.419281005859375
text3_compress_bit_gzip= 2.6956710815429688
text_compress_bit_lzma= 1.091705322265625
text2_compress_bit_lzma= 0.39215087890625
text3_compress_bit_lzma= 2.5360107421875
```

## 追記：
https://quixdb.github.io/squash-benchmark/

によれば圧縮率が最も優秀なのは「zpaq」である。``pip install pyzpaq``でインストールし、圧縮の結果は以下であった。理論エントロピーにかなり近い圧縮が達成できている。

```python
import zpaq

text_compress = zpaq.compress(text)
text2_compress = zpaq.compress(text2)
text3_compress = zpaq.compress(text3)
----------------------------------------------------------
text_compress_bit_zpaq= 1.003509521484375
text2_compress_bit_zpaq= 0.29022979736328125
text3_compress_bit_zpaq= 2.2901153564453125
```



## BitNetのエントロピー
BitNetのb1.58は{-1,0,1}であるがそれぞれの範囲は{[-1.5,-0.5], [-0.5,0.5], [0.5, 1.5]}であるからそれぞれの割合は$\beta=0.798\sigma$として[34.5%, 31%, 34.5%]
$-(0.345\*log2(0.345)+0.31\*log2(0.31)+0.345\*log2(0.345))=1.58bit$なので変わらない。
しかし、さらに等間隔に分割数を増やした展開を考えた場合
分割数4[21.2%, 28.8%, 28.8%, 21.2%]=1.98bit
分割数5[11.5%, 23%, 31%, 23%, 11.5%]=2.22bit
分割数6[4.5%, 16.7%, 28.8%,28.8%, 16.7%, 4.5%]=2.299bit
分割数7[2.3%, 9.2%, 23%, 31%, 23%, 9.2%, 2.3%]=2.383bit
分割数8[0.8%, 3.7%, 16.7%, 28.8%, 28.8%, 16.7%, 3.7%, 0.8%]=2.304bit
$-(0.008\*log2(0.008)+0.037\*log2(0.037)+0.167\*log2(0.167)+0.288\*log2(0.288))\*2=2.304$
となり、量子化が8分割であっても3bitではなく実装の工夫次第で2.3bit相当で示しうる。
前述のsalient_posを0.3bitで持てるというのはこれに似た意味になる。

# まとめ
BiLLMの特徴は5%～10%のSalientWeightを別に考える事とsalient_posをgroup化する事で2.1bitで量子化を考えることが出来る。論文中はsparse_posは演算には影響ないから考えなくてよくて1.1bitに出来ると言っているがその理屈はよく分からなかった。

SDXLでやったがgroup化有りの2.1bit量子化でbitlinearの2bit量子化(4分割)より若干改善する程度である。**group化無しの3.1bit量子化では他の量子化と比べて特に改善は見られなかった。**
少なくともSDXLでは外れ値をsalient_weightとして分割して持つ意味は特にないように感じる。出現頻度5%のsalient_posを0.3bit程度で持つことは可能だが、それは等間隔で8分割した場合もエントロピー的には同じようになる。

ヘッセ行列とか残差量子化とかp最適化をやらなかったがその辺が重要なのだろうか？
ヘッセ行列がどうこうは多分、下記のGPTQとかの手法に書いてあるが理解できていない。

数式を眺めた感じ、元の重みを$W_0$、新しい重みを$W_{new}$とした時、$L=argmin (W_0x-W_{new}x)^2$かつ$L2=argmin (quant(W_{new})-W_{new})^2$を満たす$W_{new}$を適当な入力$x$を使いつつ求めるという意味なのだろうか？

https://towardsdatascience.com/4-bit-quantization-with-gptq-36b0f4f02c34