가중치를 4분의 1로 줄이면 무엇이 사라지나
2편 끝에서 캐시가 가중치보다 커졌다. 이번 편은 반대쪽, 가중치를 줄인다.
방법은 단순하다. float32 하나가 4바이트인데, 값의 범위를 알면 정수 하나로 바꿔 담을 수 있다.
qmax = 2 ** (bits - 1) - 1
scale = w.abs().max() / qmax
q = (w / scale).round().clamp(-qmax - 1, qmax) # 정수
w_hat = q * scale # 다시 펼친 값
scale 하나를 float 로 들고 나머지를 int8 로 담으면 4바이트가 1바이트가 된다.
문제는 round 에서 버린 것이 얼마나 아픈가다.
int8 은 거의 공짜다
13편 모델을 그대로 쓴다. float32 검증 손실이 2.0272 다.
행렬당 한 스케일 출력채널당 스케일
8비트 2.0290 2.0280
float32 는 2.0272
+0.0017 이다. 소수점 셋째 자리에서 겨우 보인다. 63만 개 가중치를 전부
256단계로 뭉갰는데 모델은 거의 눈치를 못 챈다.
메모리는 이만큼 준다.
2차원 가중치 631,808 개 2.41 MB (fp32) -> 0.60 MB (int8)
전체 637,156 개 2.43 MB -> 0.62 MB 74.4% 감소
더 줄이면 어디서 무너지나
비트 행렬당 한 스케일 출력채널당 스케일
8 2.0290 2.0280
6 2.0599 2.0387
4 2.6380 2.3571
3 4.3960 3.4381
2 5.7894 6.3112
6 비트까지는 여전히 소수점 둘째 자리 얘기다. 4 비트에서 2.36 으로 벌어지고
3 비트에서 3.44 로 무너진다. 1편에서 바이그램이 2.6501 이었으니, 4비트로
줄인 모델은 앞 글자만 세는 것보다 겨우 나은 정도가 된다.
2비트에서 뒤집힌다
표의 마지막 줄이 이상하다. 2 비트에서는 채널별 스케일이 6.3112 로 행렬당
하나인 5.7894 보다 나쁘다. 세 줄 위까지는 계속 좋았는데.
무슨 일인지 재 보면 이렇다. blocks.0.f1 하나만 보면
행렬 전체 스케일 0 으로 죽은 원소 95.2% 평균절대오차 0.0519
채널별 스케일 0 으로 죽은 원소 78.1% 평균절대오차 0.0419
가중치 복원은 채널별이 더 잘했다. 죽은 원소도 적고 오차도 작다. 그런데 손실은 더 나쁘다.
즉 2비트 지점에서는 “가중치를 얼마나 잘 복원했나” 와 “모델이 얼마나 잘 동작하나” 가 이미 갈라져 있다. 둘 다 못 쓰는 상태이고, 그 안에서의 순위는 의미가 없다. 재서 나온 숫자를 그대로 적되 거기서 결론을 끌어내지는 않는 편이 맞다.
어디가 예민한가
한 번에 다 줄이는 대신 행렬 하나만 4비트로 바꾸고 나머지를 float32 로 두면 누가 아픈지 보인다.
tok.weight 손실 +0.0976
pos.weight 손실 +0.0618
blocks.2.f1.weight 손실 +0.0184
blocks.0.f2.weight 손실 +0.0174
blocks.2.qkv.weight 손실 +0.0132
...
blocks.2.f2.weight 손실 -0.0020 (가장 둔감, 오히려 조금 내려간다)
임베딩 두 개가 압도적이다. 글자 임베딩 하나가 블록 안 어떤 행렬보다 다섯 배 넘게 아프다. 실제 양자화 구현이 임베딩과 출력 층을 높은 정밀도로 남겨 두는 이유가 이것이다.
가장 둔감한 쪽이 -0.0020 으로 음수인 것도 적어 둔다. 양자화가 우연히 도움이
된 게 아니라, 이 정도 크기는 손실 측정 자체의 흔들림 안이다.
개별 손해를 다 더하면 +0.2830 인데 전부 4비트로 하면 +0.3299 다. 손해가 조금
더해지기만 하는 게 아니라 서로를 키운다.
손실은 안 움직여도 글은 달라진다
int8 은 손실을 +0.0017 만 올렸다. 그럼 같은 글이 나오나. 같은 씨앗으로 뽑아
보면
float32 '`16, a=0, 36, 112003 0.3009849\n```\n\nThe smallest `1.000h`. **`shape(2'
int8 '`16, a=0, 36, 112003 20350 0.63103 20.013910 0.016693939 16\n9 = '
스물두 번째 글자에서 갈린다. 거기까지는 같고 그 뒤로는 완전히 다른 글이다.
당연하다. 1편에서 본 대로 자리마다 후보가 붙어 있고, 확률 차이가 아주 작은
자리에서는 0.0017 어치 흔들림이 다른 글자를 뽑게 만든다. 그리고 한 글자가
달라지면 그 뒤 문맥이 전부 달라진다.
2편에서 캐시의 9.06e-06 을 두고 “비트 단위로 같지 않다” 고 적었던 것이 여기서
눈에 보이는 결과로 나타난다. 평균 성능이 같다는 것과 같은 출력이 나온다는 것은
다른 얘기다.
이 모델에서는 per-channel 이 별로 안 중요했다
양자화 이야기에서 보통 강조되는 것이 이상치다. 어떤 채널의 범위가 유별나게 크면 행렬당 스케일 하나로는 나머지 전부가 뭉개진다. 그래서 채널별로 나눈다.
이 모델에서 재 보면 그 근거가 약하다.
채널 범위의 최대/중앙 비 가장 심한 행렬 2.49 전체 중앙값 1.51
1.5 배쯤이다. 이 정도면 스케일 하나로 덮어도 크게 손해가 아니고, 실제로
8비트에서 두 방식 차이가 0.0010 이다.
활성값 쪽도 마찬가지다.
블록0 FFN 입력 채널 최대/중앙 1.50 최대 5.24
블록2 FFN 입력 채널 최대/중앙 1.54 최대 6.24
잔차 흐름(마지막) 채널 최대/중앙 1.64 최대 6.67
큰 모델에서 활성값 양자화가 어려운 이유로 꼽히는 것이 몇몇 채널이 나머지의
수십 배에서 백 배까지 튀는 현상인데, 여기서는 1.6 배다. 즉 이 실험은 그
문제를 재현하지 못한다. 13편 모델이 작고 6만 자로 5000걸음 돌린 것이라서
그렇다고 보는 게 맞고, 그래서 이 편의 결론 중 “per-channel 이 별로 안 낫다” 는
부분은 이 모델에 한정된 것이다.
그래서
- int8 은 거의 공짜다. 검증 손실
2.0272→2.0290, 메모리2.43 MB→0.62 MB,74.4%감소 6비트까지 버틴다.4비트는2.3571로 바이그램2.6501을 겨우 넘고,3비트는 무너진다2비트에서는 가중치를 더 잘 복원한 쪽이 손실은 더 나쁘다. 두 지표가 이미 갈라진 자리라 순위에 의미가 없다- 임베딩이 가장 예민하다. 4비트로 줄이면
tok이+0.0976, 블록 안 어떤 행렬보다 다섯 배 아프다 - 손실이
+0.0017이어도 뽑은 글은 22번째 글자에서 갈린다. 평균이 같은 것과 같은 출력은 다르다 - 이 모델에는 이상치가 없다. 채널 범위 비가
1.5배라 per-channel 의 이점이 작고, 그래서 이 결론은 큰 모델로 옮길 수 없다
세 번 다 “이론이 약속한 것” 과 “실제로 받은 것” 이 달랐다.
다음 편에서는 여기서 만든 4비트 모델을 버리지 않고 쓴다. 싼 모델이 미리 써 둔 글자를 비싼 모델이 한 번에 검사하는 방식인데, 출력이 정말 안 바뀌는지부터 잰다.
Comments