Model Nicemleme’yi (Quantization’ı) Derinlemesine Öğreniyoruz
Deeplearning.ai’ın “Quantization in Depth” kursunun Türkçe çevirisidir.
For English:
Giriş
Büyük Dil Modellerini (Large Language Models) nicemlemeyi öğreneceksiniz. Asimetrik ve simetrik modlar olarak adlandırılan en yaygın doğrusal nicem varyantlarını uygulayacaksınız. Bu modlar, sıkıştırma algoritmasının orijinal temsildeki sıfırı, açılmış (unpacked) temsildeki sıfıra mı eşlediği yoksa sıfırın yerini değiştirmesine izin verilip verilmediği ile ilgilidir. Ayrıca, modelinizin ne kadar büyük bir bölümünü bir seferde nicemlemek istediğinize karar verebileceğiniz PyTorch kullanarak, tensor başına, kanal başına ve grup başına kuantizasyonu gibi farklı nicem türlerini de uygulayacaksınız. Kanal başına doğrusal nicem kullanarak, herhangi bir modeli 8-bit hassasiyetle nicemlemek için bir nicemleyici oluşturacağız.
Genel Bakış
Nicemleme yöntemleri, modelleri daha küçük hale getirmek ve bu sayede YZ topluluğu için daha erişilebilir hale getirmek için kullanılır. Nicemlemenin ne olduğuna ve nasıl çalıştığına dair bir bakış atalım.
Nicemleme, modelin parametrelerini daha düşük hassasiyette depolamaktır. Bilgi distilasyonu (knowledge distillation) ile, orijinal daha büyük öğretici modeli kullanarak daha küçük bir öğrenci modeli eğitebilirsiniz (ancak bu konu bu yazı kapsamında ele alınmayacaktır). Budama, modelden ağırlıkları olan bağlantıları kaldırır (bu da bu yazıda ele alınmayacaktır). Ayrıca, makine öğreniminde yaygın olarak kullanılan veri tiplerini, örneğin INT8 veya float gibi, ele aldık. Huggingface’in quantum kütüphanesini kullanarak birkaç satır kod ile doğrusal nicemleme yapacağız. Son olarak, nicemlemenin kullanım alanlarından bahsettik. Bu kursu izledikten sonra kendi 8-bit doğrusal nicemleyicinizi oluşturabilir ve gerçek modellere uygulayabilirsiniz. Model doğrusal olduğu sürece, metin, ses veya görüntü modeli olsa bile doğrusal nicemleyicinizi herhangi bir model için kullanabilirsiniz. PyTorch, 2-bit veya 4-bit hassasiyetli ağırlıkları desteklememektedir. Bu sorunu ele almanın bir yolu, düşük hassasiyetli ağırlıkları INT8 gibi daha yüksek hassasiyetli tensörlere paketlemektir. Paketleme ve açma algoritmalarıyla bunları öğreneceğiz.
Bir Tensörü Nicemleme ve Nicemlemeyi Kaldırma
Doğrusal nicemlemenin asimetrik versiyonunu sıfırdan uygulayalım ve ölçekleme faktörü ile sıfır noktasını öğrenelim.
Nicemleme, büyük bir kümeyi daha küçük bir değerler kümesine eşlemeyi ifade eder. Birçok nicemleme yöntemi mevcuttur, ancak bu yazıda yalnızca doğrusal nicemlemeye odaklanacağız.
Solda, 8 bitlik nicemlemeyi ve torch.float32'de torch.int8'e depolanan değerleri (örneğin [-128, 127] arasında) görebilirsiniz.
Hem nicemlenmiş tensörü hem de orijinal tensörü elde etmeyi öğreneceğiz. Sinir ağı nicemlemesinde ağırlıkları (sinir ağı parametrelerini) ve aktivasyonları (sinir ağının katmanları arasında yayılan değerleri) nicemleyebilirsiniz. Eğer sinir ağını i’inci eleman eğitildikten sonra nicemlerseniz, eğitim sonrası nicemleme (PTQ) yapıyorsunuz demektir.
Nicemlemenin avantajları, daha küçük bir model elde etmeniz, bellek bant genişliğinin azalması ve işlemlerin (örneğin, GEMM: Genel Matris Çarpımın ve GEMV: Matristen Vektöre Çarpımın) daha hızlı yapılabilmesi nedeniyle gecikmenin azalmasıdır. Nicemleme ile ilgili birçok zorluk vardır, bunlar arasında nicemleme hatası, yeniden eğitim (nicemleme farkındalıklı eğitim), sınırlı donanım desteği, kalibrasyon veri seti ihtiyacı ve paketleme/paketten çıkarma yer alır.
Nicemlenmiş tensörü elde etmek için q’yu izole etmeniz gerekir.
q = int(round(r/s + z))
Şimdi, doğrusal nicemleme teorisine dalalım. Doğrusal nicemleme, örneğin float 32'yi bir tam sayıya haritalamak (mapping) için doğrusal eşlemeyi kullanır. Doğrusal nicemlemede 2 parametre vardır.
Hızlıca bir örneğe bakalım:
s=2 ve z=0 için r= 2(q — 0) = 2q buluruz. q=10 için r= 2*10=20 buluruz.
Rastgele Ölçek ve Sıfır Noktası ile Nicemleme (Quantization with Random Scale and Zero Point)
import torch
def linear_q_with_scale_and_zero_point(tensor, scale, zero_point, dtype = torch.int8):
scaled_and_shifted_tensor = tensor / scale + zero_point
rounded_tensor = torch.round(scaled_and_shifted_tensor)
q_min = torch.iinfo(dtype).min
q_max = torch.iinfo(dtype).max
q_tensor = rounded_tensor.clamp(q_min,q_max).to(dtype)
return q_tensor
### a dummy tensor to test the implementation
test_tensor=torch.tensor(
[[191.6, -13.5, 728.6],
[92.14, 295.5, -184],
[0, 684.6, 245.5]]
)
### these are random values for "scale" and "zero_point"
### to test the implementation
scale = 3.5
zero_point = -70
quantized_tensor = linear_q_with_scale_and_zero_point(
test_tensor, scale, zero_point)
print(quantized_tensor)
Rastgele Ölçek ve Sıfır Noktası ile Nicemleme (Dequantization with Random Scale and Zero Point)
dequantized_tensor = scale * (quantized_tensor.float() - zero_point)
# this was the original tensor
# [[191.6, -13.5, 728.6],
# [92.14, 295.5, -184],
# [0, 684.6, 245.5]]
print(dequantized_tensor)
### without casting to float
scale * (quantized_tensor - zero_point)
def linear_dequantization(quantized_tensor, scale, zero_point):
return scale * (quantized_tensor.float() - zero_point)
dequantized_tensor = linear_dequantization(quantized_tensor, scale, zero_point)
print(dequantized_tensor)
Nicemleme Hatası
Ortalama Karesel Hata tekniğini kullanarak “genel” bir nicemleme hatası hesaplayın.
from helper import plot_quantization_errors
plot_quantization_errors(test_tensor, quantized_tensor, dequantized_tensor)
dequantized_tensor - test_tensor
(dequantized_tensor - test_tensor).square()
(dequantized_tensor - test_tensor).square().mean()
Ölçek ve Sıfır Noktası Değerlerini Elde Etmek (Getting the Scale and Zero Point)
r = s*(q-z) denklemindeki s (ölçek) ve z (sıfır noktası) değerleri nasıl belirlenir? Doğrusal nicemleme, kayan nokta (floting point) aralığını [r_min, r_max] nicemlenmiş aralığa [q_min, q_max] eşler.
Uç değerlere baktığımızda şunu elde etmeliyiz:
İlk denklemi ikinciden çıkarırsak ölçek s’yi elde ederiz:
s = (r_max — r_min) / (q_max — q_min)
Sıfır noktası z için, n bitlik bir tam sayı olduğundan değeri yuvarlamamız gerekir:
z = int(round(q_min — r_min/s))
Orijinal ‘r’ aralığındaki sıfırı, nicemlenmiş ‘q’ aralığında bir tam sayı ile temsil edin. Örneğin, evrişimli sinir ağlarındaki sıfır dolgu (padding), tam sıfır olan tensörler kullanır. Şimdi, ölçek ve sıfır noktasını nasıl hesapladığımızı bir örnekle görelim: İlk olarak, orijinal tensörün maksimum ve minimum aralığını elde etmemiz gerekiyor. Minimum değeri -184, maksimum değeri ise 728.6 olarak ayarlıyoruz. Çünkü INT8 ile nicemliyoruz, minimum değer -128, maksimum değer ise 127'dir. r = s*(q — z) formülünü kullanarak sonucu elde ediyoruz.
Sıfır noktası aralık dışında olduğunda:
- 1. Durum (z < q_min): z=q_min olarak ayarlıyoruz.
- 2. Durum (z > q_max): z = q_max olarak ayarlıyoruz.
import torch
from helper import linear_q_with_scale_and_zero_point, linear_dequantization, plot_quantization_errors
### Uygulamayı test etmek için bir örnek tensör
test_tensor=torch.tensor(
[[191.6, -13.5, 728.6],
[92.14, 295.5, -184],
[0, 684.6, 245.5]]
)
# Nicemleme için ölçek ve sıfır noktası bulma
q_min = torch.iinfo(torch.int8).min
q_max = torch.iinfo(torch.int8).max
print(q_min) # -128
print(q_max) # 127
r_min = test_tensor.min().item()
r_max = test_tensor.max().item()
print(r_min) # -184.0
print(r_max) # 728.5999755859375
scale = (r_max - r_min) / (q_max - q_min)
zero_point = q_min - (r_min / scale)
print(scale) # 3.578893433670343
print(zero_point) # -76.58645490333825
zero_point = int(round(zero_point))
print(zero_point) # - 77
Tüm bunları bir fonksiyona koyarsak:
def get_q_scale_and_zero_point(tensor, dtype=torch.int8):
q_min, q_max = torch.iinfo(dtype).min, torch.iinfo(dtype).max
r_min, r_max = tensor.min().item(), tensor.max().item()
scale = (r_max - r_min) / (q_max - q_min)
zero_point = q_min - (r_min / scale)
# zero_point'i keserek [quantized_min, quantized_max] aralığına düşüyoruz
if zero_point < q_min:
zero_point = q_min
elif zero_point > q_max:
zero_point = q_max
else:
# Yuvarla ve int'e çevir
zero_point = int(round(zero_point))
return scale, zero_point
Şimdi başlangıçta tanımladığımız test_tensor’ü kullanarak uygulamayı test edelim:
new_scale, new_zero_point = get_q_scale_and_zero_point(test_tensor)
print(new_scale) # 3.578823433670343
print(new_zero_point) # -77
Hesaplanmış Ölçek ve Sıfır Noktası ile Nicemleme ve Nicemleme Kaldırılma (Quantization and Dequantization with Calculated Scale and Zero Point)
Hesaplanan scale ve zero_point’i, linear_q_with_scale_and_zero_point ve linear_dequantization fonksiyonlarıyla birlikte kullanalım.
quantized_tensor = linear_q_with_scale_and_zero_point(test_tensor, new_scale, new_zero_point)
dequantized_tensor = linear_dequantization(quantized_tensor,new_scale, new_zero_point)
Hesaplanan ölçek ve sıfır_noktası kullanıldıktan sonra nicemleme hatasının nasıl göründüğünü görmek için grafik oluşturalım:
plot_quantization_errors(test_tensor, quantized_tensor, dequantized_tensor)
(dequantized_tensor-test_tensor).square().mean() # tensor(1.5730)
Hepsini bir araya getirirsek doğrusal nicemleyicimiz:
def linear_quantization(tensor, dtype=torch.int8):
scale, zero_point = get_q_scale_and_zero_point(tensor,
dtype=dtype)
quantized_tensor = linear_q_with_scale_and_zero_point(tensor,
scale,
zero_point,
dtype=dtype)
return quantized_tensor, scale , zero_point
# Uygulamanızı rastgele bir matriste test ediyoruz
r_tensor = torch.randn((4, 4))
print(r_tensor)
"""
tensor([[ 0.6859, 1.2172, 0.0154, -1.3982],
[-0.5769, -0.8755, -1.6292, 3.1698],
[-1.2492, 0.9837, -0.5668, 1.0646],
[ 2.3798, -1.2179, 0.6119, -0.9990]])
"""
quantized_tensor, scale, zero_point = linear_quantization(r_tensor)
print(quantized_tensor)
"""
tensor([[ -5, 24, -40, -115],
[-72, -88, -128, 127],
[-107, 11, -71, 16],
[ 85, -106, -8, -94]], dtype=torch.int8)
"""
print(scale) # 0.018819578021180398
print(zero_point) # -41
dequantized_tensor = linear_dequantization(quantized_tensor,
scale, zero_point)
plot_quantization_errors(r_tensor, quantized_tensor,
dequantized_tensor)
(dequantized_tensor-r_tensor).square().mean()
Simetrik ve Asimetrik Mod (Symmetric vs Asymmetric Mode)
Simetrik ve asimetrik modlar hakkında bilgi edinecek ve tensör başına, kanal başına ve grup başına gibi farklı ayrıntı seviyelerinde kuantizasyonu uygulayacaksınız. Son olarak, nicemlenmiş doğrusal katmanda çıkarım yapmayı göreceksiniz.
Doğrusal nicemlemenin 2 modu vardır:
- Simetrik Doğrusal: [-rmax, rmax]’i [-qmax, qmax]’e eşliyoruz ve burada rmax = max(|r_tensor|)’ü ayarlayabiliriz.
Sıfır noktasını (z=0) kullanmamıza gerek yok. Bu, kayan nokta ve nicemlenmiş aralıkların sıfıra simetrik olmasından kaynaklanır. Bu nedenle denklemi şu şekilde basitleştirebiliriz:
- Asimetrik Doğrusal: [rmin, rmax]’ı [qmin, qmax]’a eşliyoruz. Bu, önceki bölümde uyguladığımız şeydir.
Simetrik Doğrusal Nicemleme (Symmetric Linear Quantization)
import torch
# Returns the scale for Linear Quantization in Symmetric Mode.
def get_q_scale_symmetric(tensor, dtype=torch.int8):
r_max = tensor.abs().max().item()
q_max = torch.iinfo(dtype).max
# return the scale
return r_max/q_max
### test the implementation on a 4x4 matrix
test_tensor = torch.randn((4, 4))
get_q_scale_symmetric(test_tensor) # 0.01664387710451141
Simetrik modda doğrusal nicemleme gerçekleştirelim. linear_q_with_scale_and_zero_point, önceki derste uyguladığınız fonksiyonla aynıdır.
from helper import linear_q_with_scale_and_zero_point
def linear_q_symmetric(tensor, dtype=torch.int8):
scale = get_q_scale_symmetric(tensor)
quantized_tensor = linear_q_with_scale_and_zero_point(tensor,
scale=scale,
zero_point=0, # in symmetric quantization zero point is = 0
dtype=dtype)
return quantized_tensor, scale
quantized_tensor, scale = linear_q_symmetric(test_tensor)
Nicemleme Kaldırma
Nicemlemeyi gerçekleştirip nicemleme hatasını grafikle gösterelim, linear_dequantization önceki derste uyguladığınız fonksiyonla aynıdır.
from helper import linear_dequantization, plot_quantization_errors
from helper import quantization_error
dequantized_tensor = linear_dequantization(quantized_tensor,scale,0)
plot_quantization_errors(test_tensor, quantized_tensor, dequantized_tensor)
print(f"""Quantization Error : {quantization_error(test_tensor, dequantized_tensor)}""") # Quanztization Error: 2.5686615117592737e-05
Ödünleşim (Trade-off):
- Nicemlenmiş aralığın kullanımı: Asimetrik nicemleme kullanıldığında, nicemlenmiş aralığın hepsi kullanılır. Simetrik modda, eğer kayan noktalı aralık bir tarafa doğru önyargılıysa, bu durumda nicemlenmiş aralıkta hiçbir zaman görmeyeceğimiz değerlere ayrılmış bir bölüm olacaktır (örneğin, çıktının pozitif olduğu ReLU).
- Basitlik: Simetrik mod, asimetrik moda kıyasla çok daha basittir.
- Bellek: Simetrik nicemlemede sıfır noktasını saklamayız.
Daha Fazla Hassasiyet İçin Daha İnce Granülarite (Finer Granularity for more Precision)
Nicemleme ne kadar ayrıntılı olursa, o kadar doğru sonuç verir. Ancak, daha fazla nicemleme parametresi saklamamız gerektiği için daha fazla bellek gerektirir. Nicemlemede farklı ayrıntı düzeyleri vardır. Örneğin, tensör başına nicemleme yapabiliriz, ancak gördüğünüz gibi, tüm bir tensör için aynı ölçek ve sıfır noktasını kullanmak zorunda değiliz. Örneğin, her eksen için bir ölçek ve sıfır noktası hesaplayabiliriz. Bu yönteme eksen başına nicemleme denir. Ayrıca, n elemandan oluşan bir grubu seçip her bir grubun ölçek ve sıfır noktalarını hesaplayarak bu grubu kendi ölçek ve sıfır noktalarıyla nicemleyebiliriz.
import torch
from helper import linear_q_symmetric, get_q_scale_symmetric, linear_dequantization
from helper import plot_quantization_errors, quantization_error
Nicemleme için farklı ayrıntı düzeyleri uygulayalım ancak basitlik açısından simetrik modu kullanalım.
Tensör Başına Simetrik Nicemleme (Per Tensor Symmetric Quantization)
# test tensor
test_tensor=torch.tensor(
[[191.6, -13.5, 728.6],
[92.14, 295.5, -184],
[0, 684.6, 245.5]]
)
quantized_tensor, scale = linear_q_symmetric(test_tensor)
dequantized_tensor = linear_dequantization(quantized_tensor, scale, 0)
plot_quantization_errors(test_tensor, quantized_tensor, dequantized_tensor)
print(f"""Quantization Error : {quantization_error(test_tensor, dequantized_tensor)}""")
# Quantization Error: 2.5091912746429443
Kanal Başına Nicemleme (Per Channel Quantization)
Nicemlemeyi satırlar boyunca yapmaya karar verirsek, her bir satır için ölçekleri ve sıfır noktasını saklamamız gerekir ve sütunlar boyunca yapmaya karar verirsek, bunları her sütun boyunca saklamamız gerekir. Bu doğrusal parametreleri saklamak için gereken bellek oldukça küçüktür. Genellikle 8-bit’lik model nicemlemesinde kanal başına nicemleme kullanırız. Bir sonraki derste birimlerin kullanıldığını göreceksiniz.
Şimdi kanal başına simetrik nicemlemeyi uygulayalım. dim
parametresi, işlemin satırlar mı yoksa sütunlar mı boyunca yapılacağını belirler.
import torch
from helper import get_q_scale_symmetric, linear_q_with_scale_and_zero_point, linear_dequantization
from helper import plot_quantization_errors, quantization_error
def linear_q_symmetric_per_channel(tensor,dim,dtype=torch.int8):
return quantized_tensor, scale
test_tensor=torch.tensor(
[[191.6, -13.5, 728.6],
[92.14, 295.5, -184],
[0, 684.6, 245.5]]
)
dim=0
output_dim = test_tensor.shape[dim]
print(output_dim)
scale = torch.zeros(output_dim)
print(scale) # tensor([5.7370, 2.3268, 5.3906])
scale_shape = [1] * test_tensor.dim()
print(scale_shape) # [1, 1]
scale_shape[dim] = -1
print(scale_shape) # [1, 1]
scale = scale.view(scale_shape)
copy_scale = scale # copied to be used later
m = torch.tensor([[1,2,3],[4,5,6],[7,8,9]])
print(m) # tensor([[1,2,3], [4,5,6], [7,8,9]])
s = torch.tensor([1,5,10])
print(s) # tensor([1, 5, 10])
print(s.shape) # torch.Size([3])
s.view(1, 3).shape
s.view(1, -1).shape # alternate way
s.view(-1,1).shape
scale = torch.tensor([[1], [5], [10]])
print(scale.shape) # torch.Size([3, 1])
print(m / scale) # tensor([[1.0000,2.0000,3.0000], [0.8000,1.0000,1.2000], [0.7000,0.8000,0.9000]])
scale = torch.tensor([[1, 5, 10]])
print(scale.shape) # torch.Size([1, 3])
print(m / scale) # tensor([[1.0000,0.4000,0.3000], [4.0000,1.0000,0.6000], [7.0000,1.6000,0.9000]])
# the scale you got earlier
scale = copy_scale
print(scale) # tensor([[5.7370],[2.3268],[5.3906]])
print(scale.shape) # torch.Size([3,1])
quantized_tensor = linear_q_with_scale_and_zero_point( test_tensor, scale=scale, zero_point=0)
print(quantized_tensor) # tensor([[33, -2 , 127],[40, 127, -79],[0, 127, 46]])
def linear_q_symmetric_per_channel(r_tensor, dim, dtype=torch.int8):
output_dim = r_tensor.shape[dim]
# store the scales
scale = torch.zeros(output_dim)
for index in range(output_dim):
sub_tensor = r_tensor.select(dim, index)
scale[index] = get_q_scale_symmetric(sub_tensor, dtype=dtype)
# reshape the scale
scale_shape = [1] * r_tensor.dim()
scale_shape[dim] = -1
scale = scale.view(scale_shape)
quantized_tensor = linear_q_with_scale_and_zero_point(
r_tensor, scale=scale, zero_point=0, dtype=dtype)
return quantized_tensor, scale
test_tensor=torch.tensor(
[[191.6, -13.5, 728.6],
[92.14, 295.5, -184],
[0, 684.6, 245.5]]
)
### sıralar boyunca (dim = 0)
quantized_tensor_0, scale_0 = linear_q_symmetric_per_channel(
test_tensor, dim=0)
### sütunlar boyunca (dim = 1)
quantized_tensor_1, scale_1 = linear_q_symmetric_per_channel(
test_tensor, dim=1)
# Satırlara göre niceleme hatasını çizdirelim
dequantized_tensor_0 = linear_dequantization(quantized_tensor_0, scale_0, 0)
plot_quantization_errors(test_tensor, quantized_tensor_0, dequantized_tensor_0)
print(f"""Quantization Error : {quantization_error(test_tensor, dequantized_tensor_0)}""")
# Nicemleme Hatası: 1.8084441423416138
dequantized_tensor_1 = linear_dequantization(quantized_tensor_1, scale_1, 0)
plot_quantization_errors(test_tensor, quantized_tensor_1, dequantized_tensor_1, n_bits=8)
print(f"""Quantization Error : {quantization_error(test_tensor, dequantized_tensor_1)}""")
# Nicemleme Hatası: 1.0781488418579102
“Halüsinasyonlar, büyük dil modelleri için önemli bir zorluk olmaya devam etmektedir. Modeller, bilgileri az olan alanlarda bile aşırı özgüvenle cevap verme eğilimindedir. Bu eksikliklere rağmen, genellikle bilgi tabanı olarak kullanılırlar ve bu da yanlış bilgilendirme gibi riskli sonuçlara yol açabilir. Gerçekliğin halüsinasyonların ötesine geçebileceğini kabul etsek de, burada halüsinasyon odaklı bir yaklaşım benimsedik.
Grup Başına Nicemleme (Per Group Quantization)
Grup başına nicemleme çok daha fazla bellek gerektirebilir. Diyelim ki bir tensörü 4-bit olarak nicemlemek istiyoruz ve group_size=32
, simetrik mod (z=0) seçiyoruz ve ölçekleri FP16 formatında saklıyoruz. Bu, tensörü 4,5 bit olarak nicemlediğimiz anlamına gelir çünkü:
- 4-bit (her bir eleman 4-bit olarak saklanır)
- 16/32 bit (her 32 eleman için 16-bit ölçek)
Uygulayalım. Basitlik için, kendimizi tensörün iki boyutlu olduğu duruma sınırlayacağız ve simetrik modu kullanacağız.
import torch
from helper import linear_q_symmetric_per_channel, get_q_scale_symmetric, linear_dequantization
from helper import plot_quantization_errors, quantization_error
Basitleştirmek adına, satırlar boyunca 2 boyutlu bir tensörü nicemleyeceğiz.
def linear_q_symmetric_per_group(tensor, group_size, dtype=torch.int8):
t_shape = tensor.shape # to get rows of group size elements
assert t_shape[1] % group_size == 0
assert tensor.dim() == 2
tensor = tensor.view(-1, group_size)
quantized_tensor, scale = linear_q_symmetric_per_channel(tensor, dim=0, dtype=dtype)
quantized_tensor = quantized_tensor.view(t_shape)
return quantized_tensor, scale
def linear_dequantization_per_group(quantized_tensor, scale, group_size):
q_shape = quantized_tensor.shape
quantized_tensor = quantized_tensor.view(-1, group_size)
dequantized_tensor = linear_dequantization(quantized_tensor, scale, 0)
dequantized_tensor = dequantized_tensor.view(q_shape)
return dequantized_tensor
test_tensor = torch.rand((6, 6))
group_size = 3
quantized_tensor, scale = linear_q_symmetric_per_group(test_tensor, group_size=group_size)
dequantized_tensor = linear_dequantization_per_group(quantized_tensor, scale, group_size=group_size)
plot_quantization_errors(test_tensor, quantized_tensor, dequantized_tensor)
print(f"""Quantization Error : {quantization_error(test_tensor, dequantized_tensor)}""")
# Nicemleme Hatası: 2.150184179332227e^-06
Çıkarım için Ağırlıkların ve Aktivasyonların Nicemlenmesi (Quantizing Weights & Activations for Inference)
Bir sinir ağında, yalnızca ağırlıkları değil aynı zamanda aktivasyonu da nicemleyebiliriz. Neyi nicemlediğimize bağlı olarak, depolama ve hesaplama aynı değildir.
Not: Ağırlıkları, kayan nokta aritmetiğinde hesaplama yapabilmek için yeniden nicemden çıkarmamız gerekiyor ve tüm donanımlar tam sayı tabanlı aritmetiği desteklemez.
W8A32, ağırlıkları 8 bit ve aktivasyonları 32 bit olarak ifade eder. Basitlik açısından, doğrusal katman önyargısız olacaktır.
import torch
from helper import linear_q_symmetric, get_q_scale_symmetric
def quantized_linear_W8A32_without_bias(input, q_w, s_w, z_w):
assert input.dtype == torch.float32
assert q_w.dtype == torch.int8
dequantized_weight = q_w.to(torch.float32) * s_w + z_w
output = torch.nn.functional.linear(input, dequantized_weight)
return output
input = torch.tensor([1, 2, 3], dtype=torch.float32)
weight = torch.tensor([[-2, -1.13, 0.42],
[-1.51, 0.25, 1.62],
[0.23, 1.35, 2.15]])
q_w, s_w = linear_q_symmetric(weight)
print(q_w) # tensor([[-118, -67, 25],[-89, 15, 96],[14, 80, 127]], dtype=torch.int8)
print(s_w) # 0.016929134609192376
output = quantized_linear_W8A32_without_bias(input, q_w, s_w, 0)
print(f"This is the W8A32 output: {output}")
# This is the W8A32 output: tensor([-2.9965, 3.8768, 9.3957])
fp32_output = torch.nn.functional.linear(input, weight)
print(f"This is the output if we don't quantize: {fp32_output}")
# This is the output if we don't quantize: tensor([-3.000, 3.8500, 9.3800])
Kendi 8-bit Nicemleyicimizi Oluşturmak (Custom Build an 8-bit Quantizer)
W8A16LinearLayer sınıfını oluşturmayı, 8-bit ağırlıkları ve ölçekleri depolamayı öğreneceğiz. Tüm ‘torch.nn.Linear katmanlarını’ W8A16LinearLayer ile değiştirerek bir nicemleme oluşturacak ve bir modeli uçtan uca nicemleyeceğiz. Naive absmax nicemlemeyi birçok senaryoda test ederek etkilerini inceleyeceğiz.
import torch
import torch.nn as nn
import torch.nn.functional as F
random_int8 = torch.randint(-128, 127, (32, 16)).to(torch.int8)
random_hs = torch.randn((1, 16), dtype=torch.bfloat16)
scales = torch.randn((1, 32), dtype=torch.bfloat16)
bias = torch.randn((1, 32), dtype=torch.bfloat16)
F.linear(random_hs, random_int8.to(random_hs.dtype))
F.linear(random_hs, random_int8.to(random_hs.dtype)) * scales
(F.linear(random_hs, random_int8.to(random_hs.dtype)) * scales) + bias
def w8_a16_forward(weight, input, scales, bias=None):
casted_weights = weight.to(input.dtype)
output = F.linear(input, casted_weights) * scales
if bias is not None:
output = output + bias
return output
print("With bias:\n\n", w8_a16_forward(random_int8, random_hs, scales, bias))
print("\nWithout bias:\n\n", w8_a16_forward(random_int8, random_hs, scales))
W8A16linearLayer sınıfı: Kayıt tamponu (Register Buffer), bir parametre yerine bir tampon bellek saklamanın tek yoludur; bu da, o tensör üzerinde gradyan hesaplamamız gerekmediği anlamına gelir. Onu istediğiniz herhangi bir ‘dtype’ ile başlatabilirsiniz.
class W8A16LinearLayer(nn.Module):
def __init__(self, in_features, out_features,
bias=True, dtype=torch.float32):
super().__init__()
self.register_buffer("int8_weights",torch.randint(-128, 127, (out_features, in_features), dtype=torch.int8))
self.register_buffer("scales", torch.randn((out_features), dtype=dtype))
if bias:
self.register_buffer("bias", torch.randn((1, out_features), dtype=dtype))
else:
self.bias = None
def quantize(self, weights):
w_fp32 = weights.clone().to(torch.float32)
scales = w_fp32.abs().max(dim=-1).values / 127
scales = scales.to(weights.dtype)
int8_weights = torch.round(weights/scales.unsqueeze(1)).to(torch.int8)
self.int8_weights = int8_weights
self.scales = scales
def forward(self, input):
return w8_a16_forward(self.int8_weights, input, self.scales, self.bias)
module = W8A16LinearLayer(4, 8)
print("Weights before:\n" , module.int8_weights)
random_matrix = torch.randn((4, 8), dtype=torch.bfloat16)
module.quantize(random_matrix)
print("Weights After:\n" , module.int8_weights)
print(module.scales.shape) # torch.Size([4])
print(module.int8_weights.shape) # torch.Size([4,8])
dequentized_weights = module.int8_weights * module.scales.unsqueeze(1)
original_matrix = random_matrix
(original_matrix - dequentized_weights).abs().mean() # tensor(0.0045, dtpye=torch.bfloat16)
PyTorch katmanlarını Nicemlenmiş Katmanlarla değiştirme (Replace PyTorch layers with Quantized Layers)
Nicemleyicimizi oluşturmak için tüm yapı taşlarına sahibiz. Nicemleyici, orijinal modelimizin tüm doğrusal modülleri üzerinde döngü yapacak, bunları yeni W8A16 doğrusal katman modülümüzle değiştirecek ve orijinal ağırlıkları kullanarak nicemle çağrısında bulunacaktır.
Modelin yerinde doğrusal katman değiştirmesi:
import torch
import torch.nn as nn
from helper import W8A16LinearLayer
def replace_linear_with_target(module,
target_class, module_name_to_exclude):
for name, child in module.named_children():
if isinstance(child, nn.Linear) and not \
any([x == name for x in module_name_to_exclude]):
old_bias = child.bias
new_module = target_class(child.in_features,
child.out_features,
old_bias is not None,
child.weight.dtype)
setattr(module, name, new_module)
if old_bias is not None:
getattr(module, name).bias = old_bias
else:
# İç içe modüller için işlevi yinelemeli olarak çağırmak
replace_linear_with_target(
child, target_class, module_name_to_exclude)
class DummyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.emb = torch.nn.Embedding(1, 1)
# Önyargılı deneyin
self.linear_1 = nn.Linear(1, 1)
# Önyargısız deneyin
self.linear_2 = nn.Linear(1, 1, bias=False)
# Lm tahmin başlığı
self.lm_head = nn.Linear(1, 1, bias=False)
model_1 = DummyModel()
model_2 = DummyModel()
replace_linear_with_target(model_1, W8A16LinearLayer, ["lm_head"])
print(model_1)
replace_linear_with_target(model_2, W8A16LinearLayer, [])
print(model_2)
Doğrusal katman değiştirme ve nicemleme:
def replace_linear_with_target_and_quantize(module,
target_class, module_name_to_exclude):
for name, child in module.named_children():
if isinstance(child, nn.Linear) and not \
any([x == name for x in module_name_to_exclude]):
old_bias = child.bias
old_weight = child.weight
new_module = target_class(child.in_features,
child.out_features,
old_bias is not None,
child.weight.dtype)
setattr(module, name, new_module)
getattr(module, name).quantize(old_weight)
if old_bias is not None:
getattr(module, name).bias = old_bias
else:
# İç içe modüller için işlevi yinelemeli olarak çağırın
replace_linear_with_target_and_quantize(child,
target_class, module_name_to_exclude)
model_3 = DummyModel()
replace_linear_with_target_and_quantize(model_3, W8A16LinearLayer, ["lm_head"])
print(model_3)
Herhangi bir Açık Kaynaklı PyTorch Modelini Nicemleme (Quantize any Open Source PyTorch Model)
Özel nicemleyicilerimizi gerçek modeller üzerinde deneyelim.
import torch
import torch.nn as nn
import torch.nn.functional as F
from helper import W8A16LinearLayer, replace_linear_with_target_and_quantize
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
model_id = "./models/Salesforce/codegen-350M-mono"
model = AutoModelForCausalLM.from_pretrained(model_id,
torch_dtype=torch.bfloat16,
low_cpu_mem_usage=True)
tokenizer = AutoTokenizer.from_pretrained(model_id)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
print(pipe("def hello_world():", max_new_tokens=20, do_sample=False))
"""
[{'generated_text': 'def hello_world():\n print("Hello World")\n\nhello_world()\n\n# 파'}]
"""
print("Model before:\n\n", model)
replace_linear_with_target_and_quantize(model, W8A16LinearLayer, ["lm_head"])
print(pipe.model)
print(pipe("def hello_world():", max_new_tokens=20,do_sample=False)[0]["generated_text"])
"""
def hello_world():
print("Hello World")
# hello_world()
# def hello_
"""
Bir nesne algılama modelinde nicemleyicinin nasıl çağrılacağını görelim. Nesne algılama için “Detr” kullanacağız. Detr, Facebook AI tarafından tasarlanmış ve nesne algılama için kullanılan bir mimaridir.
from transformers import DetrImageProcessor, DetrForObjectDetection
from PIL import Image
import requests
# you can specify the revision tag if you don't want the timm dependency
processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50", revision="no_timm")
model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50", revision="no_timm")
previous_memory_footprint = model.get_memory_footprint()
print("Footprint of the model in MBs: ", previous_memory_footprint/1e+6)
# Footprint of the model in MBs: 166.524032
img_path = "dinner_with_friends.png"
image = Image.open(img_path).convert("RGB")
image
from helper import plot_results
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
# convert outputs (bounding boxes and class logits) to COCO API
# let's only keep detections with score > 0.9
target_sizes = torch.tensor([image.size[::-1]])
results = processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=0.9)[0]
plot_results(model, image, results)
print(model)
replace_linear_with_target_and_quantize(model, W8A16LinearLayer,[“0”, “1”, “2”, “class_labels_classifier”])
### Nicemleme sonrası model
print(model)
inputs = processor(images=image, return_tensors=”pt”)
with torch.no_grad():
outputs = model(**inputs)
# çıktıları (sınırlayıcı kutular ve sınıf logitleri) COCO API'sine dönüştür
# sadece skoru > 0.9 olan tespitleri tutalım
target_sizes = torch.tensor([image.size[::-1]])
results = processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=0.9)[0]
plot_results(model, image, results)
new_footprint = model.get_memory_footprint()
print("Footprint of the model in MBs: ", new_footprint/1e+6)
# Modelin MB cinsinden ayak izi: 114.80384
### Memory saved
print("Memory saved in MBs: ", (previous_memory_footprint - new_footprint)/1e+6)
# MB olarak kaydedilen bellek: 51.720192
HuggingFace Hub’dan Quantized Ağırlıklarınızı Yükleyin (Load your Quantized Weights from HuggingFace Hub)
Mevcut tasarımda, modeli nicemlemek için önce orijinal hassasiyetinde yüklememiz gerekiyor. Bu nedenle ideal olarak bu optimal değil çünkü modelinizi varsayılan d-tipinde yüklemek için yeterli RAM tahsis etmeniz ve ardından modeli nicemlemeniz gerekiyor. Pratikte, büyük bir örnek kullanarak modeli nicemleyebilirsiniz. Eğer yüksek hesaplama gücüne sahip bir makineniz varsa, modeli makinenizde nicemleyebilir ve ardından nicemlenmiş ağırlıkları buluta bir yere, örneğin HuggingFace hub’a yükleyebilirsiniz. Ardından modeli doğrudan makinenize 8-bit veya daha düşük hassasiyette yükleyebilirsiniz.
Aşağıdaki örnek, yüksek hesaplama gücüne sahip bir makinede olduğumuzu varsayar.
import torch
from helper import W8A16LinearLayer, replace_linear_with_target_and_quantize, replace_linear_with_target
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "./models/facebook/opt-125m"
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, low_cpu_mem_usage=True)
tokenizer = AutoTokenizer.from_pretrained(model_id)
replace_linear_with_target_and_quantize(model, W8A16LinearLayer, ["lm_head"])
print(model)
quantized_state_dict = model.state_dict()
torch.save(quantized_state_dict, "quantized_state_dict.pth")
from huggingface_hub import HfApi, create_repo
YOUR_HF_USERNAME = ""
your_repo_id = f"{YOUR_HF_USERNAME}/opt-125m-quantized-dlai"
api = HfApi()
# create_repo(your_repo_id)
api.upload_file(
path_or_fileobj="quantized_state_dict.pth",
path_in_repo="quantized_state_dict.pth",
repo_id=your_repo_id
)
Bu modeli HuggingFace’ten yüklerken Pytorch’un meta cihazını kullanacağız. Buradaki fikir, modelin tam mimarisini, doğru modülleri vb. elde etmek için önce modelin iskeletini yüklemektir. Ardından bu iskeleti yükledikten sonra, modelin nicemlenmesine gerek kalmadan, doğrusal katmanların tüm örneklerini sadece nicemlenmiş katmanlarımızla değiştirmemiz gerekiyor; çünkü tüm ağırlıklar ana cihazda olduğundan ve başlatılmadıklarından dolayı ağırlıklara erişimimiz yok.
Doğrusal katmanların hepsini değiştirdikten sonra, model.load_state_dict çağrısını, nicemlenmiş durum sözlüğünü (quantized state dict) geçirerek yapmanız yeterlidir. Bu durumda, state_dict otomatik olarak her modülde doğru ağırlıkları atayacaktır. Bu şekilde CPU RAM’inizi tasarruf edersiniz çünkü orijinal modeli yüklemenize gerek kalmaz. İlk olarak, state_dict’i yükleyerek modelin nicemlenmiş sürümünü doğrudan yüklersiniz. Ayrıca, tüm modeli yüklemek yerine sadece modelin iskeletini yüklemeniz gereken PyTorch’un meta cihazını kullanıyorsunuz. Bunu başarmak için ilk olarak modelin mimarisi hakkındaki ayrıntıları elde etmek için modelin yapılandırmasını (config) yüklüyoruz.
# Loading the model in meta device
from transformers import OPTForCausalLM, AutoTokenizer, AutoConfig
model_id = "./models/facebook/opt-125m"
config = AutoConfig.from_pretrained(model_id)
with torch.device("meta"):
model = OPTForCausalLM(config)
tokenizer = AutoTokenizer.from_pretrained(model_id)
# Parameters have tensors but they are not initialized
for param in model.parameters():
print(param) # Parameter containing: tensor(..., devide='meta', size=(50272, 768), required_grad=True)
print(model)
replace_linear_with_target(model, W8A16LinearLayer, ["lm_head"])
print(model)
from huggingface_hub import hf_hub_download
state_dict_cache_path = hf_hub_download(
"ybelkada/opt-125m-quantized-dlai",
"quantized_state_dict.pth"
)
state_dict = torch.load(state_dict_cache_path)
model.load_state_dict(state_dict, strict=True, assign=True)
# <All keys matched successfully>
Nicemlenmiş modelimizi deneyelim:
from transformers import pipeline
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
pipe("Hello today I am", max_new_tokens=40)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
pipe("Hello today I am giving a course about", max_new_tokens=10)
Ağırlık Paketleme (Weights Packing)
Ağırlık sıçramasına dalarak 2 veya 4 bit gibi düşük bitli nicemleme uygularken bazı ortak zorlukları ele alalım. Nicemlenmiş ağırlıkları depolamak için ağırlık paketlemenin neden önemli olduğunu, paketlenmiş bir uint8 tensörde 2 ve 4 bitlik ağırlıkları depolama ve yükleme ve LLMS gibi üretken modelleri nicemleme ile ilgili diğer zorlukları tartışalım.
import torch
tensor = torch.tensor8[0,1], dtype=torch.int4) # NOT SUPPORTED
tensor = torch.tensor8[0,1], dtype=torch.uint8) # SUPPORTED BUT NOT IDEAL
PyTorch’ta 8-bit kullanmak ideal değildir, çünkü tensörünüz her veri noktası başına 8-bit yer kaplayacak ve büyük modeller için önemli bir ek yük getirecektir (2/4 bit’e nicemlemek için bir anlamı olmayacaktır). Bunu çözmek için 4-bit ağırlıkları 8-bit tensörlerde paketlememiz gerekir. Aşağıdaki tensörü düşünün, 2-bit hassasiyetle temsil edilebilecek 4 değeri depolar. 2-bit hassasiyetle, 4 değeri kodlayabilirsiniz. İkili tabanda, 0, 1, 2 ve 3'ü kodlayabiliriz. En fazla dört değeri iki’nin iki’nin katına kadar kodlayabiliriz.
import torch
tensor = torch.tensor([1,0,3,2], dtype=torch.uint8)
# Encoded as: 00000001 00000000 00000011 00000010
packed_tensor = torch.Tensor([177], dtype=torch.uint8)
# Encoded as: 10110001
2-Bit Ağırlıkların Paketlenmesi (Packing 2-Bit Weights)
Int8 yerine unsigned int kullanıyoruz çünkü int8 tensörü için ilk bit tensörün işaretini belirlemek için kullanılır. Yani sadece basitlik olsun diye, işaret bitiyle uğraşmayıp unsigned bit kullanacağız.
Örnek Tensor: [1, 0, 3, 2]
1 0 3 2 - 01 00 11 10
Paketlenmiş int8 Tensor'ın başlangıç noktası
[0000 0000]
İlk İterasyon Başlangıç:
Paketlenmiş int8 Tensor durumu: [0000 0000]
1 = 0000 0001
0000 0001
İlk iterasyonda sola kaydırma yok
0000 0000 ve 0000 0001 arasında bit-bazlı OR işlemi sonrası:
Paketlenmiş int8 Tensor durumu: 0000 0001
İlk İterasyon Sonu
İkinci İterasyon Başlangıç:
Paketlenmiş int8 Tensor durumu: [0000 0001]
0 = 0000 0000
0000 0000
2 sola kaydırma:
[0000 0000] (1 kaydırma)-> 0000 0000 (2 kaydırma)-> 0000 0000
0000 0001 ve 0000 0000 arasında bit-bazlı OR işlemi sonrası:
Paketlenmiş int8 Tensor durumu: 0000 0001
İkinci İterasyon Sonu
Üçüncü İterasyon Başlangıç:
Paketlenmiş int8 Tensor durumu: [0000 0001]
3 = 0000 0011
0000 0011
4 sola kaydırma:
[0000 0011] (1 kaydırma)-> 0000 0110 (2 kaydırma)-> 0000 1100
0000 1100 (3 kaydırma)-> 0001 1000 (4 kaydırma)-> 0011 0000
0000 0001 ve 0011 0000 arasında bit-bazlı OR işlemi sonrası:
Paketlenmiş int8 Tensor durumu: 0011 0001
Üçüncü İterasyon Sonu
Dördüncü İterasyon Başlangıç:
Paketlenmiş int8 Tensor durumu: [0011 0001]
2 = 0000 0010
0000 0010
6 sola kaydırma:
[0000 0010] (1 kaydırma)-> 0000 0100 (2 kaydırma)-> 0000 1000
0000 1000 (3 kaydırma)-> 0001 0000 (4 kaydırma)-> 0010 0000
0010 0000 (5 kaydırma)-> 0100 0000 (6 kaydırma)-> 1000 0000
0011 0001 ve 1000 0000 arasında bit-bazlı OR işlemi sonrası:
Paketlenmiş int8 Tensor durumu: 1011 0001
Dördüncü İterasyon Sonu
Son paketlenmiş int8 Tensor durumu: [1011 0001]
import torch
def pack_weights(uint8tensor, bits):
if uint8tensor.shape[0] * bits % 8 != 0:
raise ValueError(f"The input shape needs to be a mutiple of {8 / bits} - got {uint8tensor.shape[0]}")
num_values = uint8tensor.shape[0] * bits // 8
num_steps = 8 // bits
unpacked_idx = 0
packed_tensor = torch.zeros((num_values), dtype=torch.uint8)
# Her num_step (sağdaki 2 bit) için karşılık gelen değeri alacağız.
# Daha sonra 8 bit olarak kodlanmış bu tensör için sol tarafta bitsel kaydırma yapacağız.
# Kaydırma bit * j kez gerçekleşecek. Daha sonra bitsel veya işlem geçerli paketlenmiş tensör üzerinde yapılacak.
"""
1 0 3 2 - 01 00 11 10
[0000 0000] -> 0000 0001
0000 0001
0000 0000 - 0000 0000
0000 0011 - 0011 0000 - 0011 0001
1011 0001
"""
for i in range(num_values):
for j in range(num_steps):
packed_tensor[i] |= uint8tensor[unpacked_idx] << (bits * j)
unpacked_idx += 1
return packed_tensor
unpacked_tensor = torch.tensor([1, 0, 3, 2], dtype=torch.uint8)
pack_weights(unpacked_tensor, 2) # tensor([177], dtype=torch.uint8)
unpacked_tensor = torch.tensor([1, 0, 3, 2, 3, 3, 3, 3], dtype=torch.uint8)
pack_weights(unpacked_tensor, 2) # tensor([177, 255], dtype=torch.uint8)
2-Bit Ağırlıkların Paketten Çıkarılması (Unpacking 2-Bit Weights)
Ağırlıkları kullanmak için paketinden çıkarmanız gerekir.
packed_tensor = torch.tensor([177], dtype=torch.uint8) # 10 11 00 01
Bunları, her 2 bitlik tam sayıyı çıkarıp uint8 tam sayılarına atayarak açmak istiyoruz:
unpacked_tensor = torch.Tensor([1,0,3,2], dtype=torch.uint8) # 00000007 00000000 00000011 00000010
Algoritma Açıklaması:
Örnek Tensor: [10110001]
Orijinal hali: 1 0 3 2 - 01 00 11 10
Paketlenmemiş Tensor'ın başlangıç noktası
[00000000 00000000 00000000 00000000]
İlk İterasyon Başlangıç:
Paketlenmiş int8 Tensor: [10110001]
[101100 01]'den 01'i çıkarmak istiyorsunuz
İlk iterasyonda sağa kaydırma yok
00000000 ve 10110001 arasında bit-bazlı OR işlemi sonrası:
[10110001 00000000 00000000 00000000]
Paketlenmemiş Tensor durumu: [10110001 00000000 00000000 00000000]
İlk İterasyon Sonu
İkinci İterasyon Başlangıç:
Paketlenmiş int8 Tensor: [10110001]
[1011 00 01]'den 00'i çıkarmak istiyorsunuz
2 sağa kaydırma:
[10110001] (1 kaydırma)-> 01011000 (2 kaydırma)-> 00101100
00000000 ve 00101100 arasında bit-bazlı OR işlemi sonrası:
[10110001 00101100 00000000 00000000]
Paketlenmemiş Tensor durumu: [10110001 00101100 00000000 00000000]
İkinci İterasyon Sonu
Üçüncü İterasyon Başlangıç:
Paketlenmiş int8 Tensor: [10110001]
[10 11 0001]'den 11'i çıkarmak istiyorsunuz
4 sağa kaydırma:
[10110001] (1 kaydırma)-> 01011000 (2 kaydırma)-> 00101100
00101100 (3 kaydırma)-> 00010110 (4 kaydırma)-> 00001011
00000000 ve 00001011 arasında bit-bazlı OR işlemi sonrası:
[10110001 00101100 00001011 00000000]
Paketlenmemiş Tensor durumu: [10110001 00101100 00001011 00000000]
Üçüncü İterasyon Sonu
Dördüncü İterasyon Başlangıç:
Paketlenmiş int8 Tensor: [10110001]
[10 110001]'den 10'u çıkarmak istiyorsunuz
6 sağa kaydırma:
[10110001] (1 kaydırma)-> 01011000 (2 kaydırma)-> 00101100
00101100 (3 kaydırma)-> 00010110 (4 kaydırma)-> 00001011
00001011 (5 kaydırma)-> 00000101 (6 kaydırma)-> 00000010
00000000 ve 00000010 arasında bit-bazlı OR işlemi sonrası:
[10110001 00101100 00001011 00000010]
Paketlenmemiş Tensor durumu: [10110001 00101100 00001011 00000010]
Dördüncü İterasyon Sonu
Son adım: Maskleme (bit-bazlı AND işlemi)
Mask: 00000011
Paketlenmemiş Tensor ve 00000011 arasında bit-bazlı AND işlemi
[10110001 00101100 00001011 00000010] <- paketlenmemiş tensor
[00000011 00000011 00000011 00000011] <- Mask
[00000001 00000000 00000011 00000010] <- Sonuç
Son
Paketlenmemiş Tensor durumu: [00000001 00000000 00000011 00000010]
def unpack_weights(uint8tensor, bits):
num_values = uint8tensor.shape[0] * 8 // bits
num_steps = 8 // bits
unpacked_tensor = torch.zeros((num_values), dtype=torch.uint8)
unpacked_idx = 0
# 1 0 3 2 - 01 00 11 10
# [00000000 00000000 00000000 00000000]
# [10110001 00101100 00001011 00000010]
# [00000001 00000000 00000011 00000010]
# 10110001
# 00000011
# 00000001
# 1: [10110001]
# 2: [00101100]
# 3: [00001011]
mask = 2 ** bits - 1
for i in range(uint8tensor.shape[0]):
for j in range(num_steps):
unpacked_tensor[unpacked_idx] |= uint8tensor[i] >> (bits * j)
unpacked_idx += 1
unpacked_tensor &= mask
return unpacked_tensor
unpacked_tensor = torch.tensor([177, 255], dtype=torch.uint8)
unpack_weights(unpacked_tensor, 2) # Answer should be: torch.tensor([1, 0, 3, 2, 3, 3, 3, 3]
Doğrusal Nicemlemenin Ötesinde
Büyük Dil Modelleri (LLM)’nin ortaya çıkan özellikleri (emergent features) nicemlemenin en büyük zorluklarından biridir. 2022'de araştırmacılar doğrudan modelin yeteneklerine daldılar ve bazı ortaya çıkan özellikler keşfettiler. Ortaya çıkan özellikler, model büyük olduğunda ölçekte ortaya çıkan özellikler veya karakteristiklerdir.
Bazı modeller için ölçekte, model tarafından tahmin edilen özellikler, yani gizli durumların büyüklüğü büyük hale geldi, bu da klasik nicemleme şemalarını oldukça eski hale getirdi, bu da klasik lineer nicemleme algoritmalarının bu modellerde başarısız olmasına yol açtı. Birçok araştırmacı, LLM’lerdeki aykırı özelliklerle nasıl başa çıkılacağını ele alma kararını aldı. Aykırı özellikler, basitçe büyük büyüklüğe sahip gizli durumlar anlamına gelir. Int8, SmoothQuant ve AWQ gibi bazı ilginç makaleler vardır.
Matmul’u 2 parçaya ayırarak nicemleme yapmak mümkündür. Aykırı kısım, belirli bir eşik değerinden büyük olan tüm gizli durumları ve aykırı olmayan kısımıdır. Yani, sekiz bitlik nicemleme yaparsınız, ardından ölçekleri kullanarak nicemlemeyi kaldırırsınız , böylece girdi veri tipinde son sonuçları alırsınız.
Bu yaklaşım, nicemleme işlemini iki parçaya ayırarak, aykırı değerlerin etkisini azaltmaya çalışır. İlk olarak, gizli durumların bir kısmını aykırı olarak tanımlar ve bu değerleri nicemlemez. Ardından, nicemleme işlemini sekiz bitlik bir formatda gerçekleştirir ve son olarak, ölçekleri kullanarak nicemlemeyi kaldırır, böylece girdi veri tipinde son sonuçları elde eder.
Başka bir ilginç yaklaşım SmoothQuant olarak adlandırılıyor. SmoothQuant özellikle A8W8 şemalarına uygulanıyor. Yani aktivasyonları da nicemlemek istiyoruz. Bu, hem aktivasyonun hem de ağırlıkların 8-bit hassasiyetinde olduğu anlamına geliyor. Makale ayrıca büyük dil modellerindeki aykırı özellikler sorununu ele alıyor. Ve bunu aktivasyonları ve ağırlıkları yumuşatarak hafifletmeyi öneriyorlar. Girdi aktivasyonuna dayalı olarak nicemleme zorluğunu nicemleme sırasında hem aktivasyonların hem de ağırlıkların nicemlendirilmesi sırasında göç ettirmek için belirlenen bir faktör verildi. Bu nedenle nicemleme zorluğunu veya hepsini eşit olarak ağırlıklara, ağırlıklara ve aktivasyona aktarıyorsunuz. Ve bu şekilde modelin tam kapasitesini de koruyabilirsiniz.
Daha yeni bir makale olan AWQ, ayrıca aykırı özelliği özel olarak ele alıyor. AWQ, önce nicemleştirme öncesi model ağırlıklarını ölçeklendirmek ve ayrıca çıkarım sırasında girdiyi yeniden ölçeklendirmek için kullanmak üzere, hangi girdi ağırlıklarının kanalı aykırı özellikler üretmekten sorumlu olabileceğini belirlemek için, nicelleştirme öncesi bir kalibrasyon veri kümesinden geçmeyi öneriyor. Bu, “önemli ağırlıklar” olarak adlandırılan aykırı özelliklerin oluşturulmasından sorumlu olabilecek kanalların ayrıntılı bir fikir edinmek için kalibrasyon veri kümesinden geçmeyi içeriyor.
Son zamanlarda ortaya çıkan SOTA nicemleştirme yöntemleri (kronolojik sırayla):
- LLM.INT8 (sadece 8-bit) — Ağustos 2022 Dettmers et al.
- GPTQ — Ekim 2022 Frantar et al.
- SmoothQuant — Kasım 2022 Xiao et al.
- QLoRA (sadece 4-bit) — Mayıs 2024 Dettmers et al.
- AWQ — Haziran 2024 Lin et al.
- QuIP# (2-bit için umut verici) Temmuz 2024 Tseng et al.
- HQQ (2-bit için umut verici) Kasım 2024 Badri et al.
- AQLM (2-bit için umut verici) Şubat 2024 Egiazarian et al.
Aşağıdakiler, modellerin çok büyük olması nedeniyle nicemleştirmeyle ilgili bazı zorluklardır:
- Eğitim (Nicelleştirme Farkındalığı Eğitimi)
- Sınırlı Donanım Desteği
- Kalibrasyon
- Veri Kümesinin Gerekli Olması
- Paketleme/Paketten Çıkarma
Daha fazla bilgi için:
- SOTA nicelleştirme makaleleri
- MIT Han Laboratuvarı
- Transformers nicelleştirme belgeleri ve blog yazıları
- llama.cpp tartışmaları
- Reddit r/LocalLlama
Kaynaklar
[1] Deeplearning.ai, (2024), Quantization in Depth:
[https://www.deeplearning.ai/short-courses/quantization-in-depth/]