추론 엔진
0b341e5 기준입니다.
BitNet은 1-bit 계열 언어 모델을 위한 공식 추론 프레임워크입니다.
저장소의 제품 이름은 bitnet.cpp입니다.
이미 학습한 모델 파일을 읽어 텍스트를 생성하고, 지원되는 임베딩 모델에서는 문장을 벡터로 바꿉니다.
이 저장소의 핵심은 작은 가중치 표현을 CPU와 GPU가 빠르게 처리할 수 있는 메모리 배치와 연산으로 연결하는 것입니다.
BitNet b1.58의 가중치는 기본적으로 {−1, 0, +1}이라는 세 상태를 사용합니다.
세 상태를 표현하는 정보량이 log₂(3) ≈ 1.585비트라서 b1.58이라고 부릅니다.
하지만 일반적인 메모리에서는 정수 개수의 비트를 묶어 다루므로, CPU I2_S 경로는 가중치 하나를 2비트 코드에 담습니다.
이름의 정보량과 파일의 실제 저장량을 구분하는 것이 첫 번째 학습 포인트입니다.
# CPU I2_S에서 사용하는 값의 대응 관계
가중치 값 −1 0 +1
저장 코드 0 1 2
2비트 표현 00 01 10
# 한 바이트에는 네 개의 2비트 코드가 들어간다
[코드 0][코드 1][코드 2][코드 3] = 8비트
# 개념 설명용 내적: 가중치의 공통 배율은 1로 가정
x = [4, 7, 2, 5]
w = [1, 0, -1, 1]
x · w = 4 + 0 - 2 + 5 = 7
실제 커널은 배율, 정수 활성값, 벡터 명령을 함께 다룹니다. 위 식은 “삼진 가중치라면 계산을 단순하게 만들 수 있다”는 원리만 보여 줍니다. 전체 Transformer의 모든 계산이 덧셈과 뺄셈으로 바뀌는 것은 아닙니다. Attention, 정규화, 토큰 임베딩 등에는 다른 정밀도의 계산이 남습니다.
가중치를 압축하는 것은 택배 상자를 작게 만드는 일입니다. 메모리에서 가져올 짐은 줄지만, 매번 상자를 풀고 원래 포장으로 바꾸면 이득이 사라질 수 있습니다.
BitNet의 커널은 작은 포장을 처리하는 전용 분류기입니다. 비트를 묶는 순서와, 묶인 비트를 꺼내 계산하는 순서를 함께 설계합니다. 그래서 파일 형식을 이해하면 성능 코드도 이해하기 쉬워집니다.
| 구분 | 무엇인가 | 이 저장소에서 만나는 곳 |
|---|---|---|
| 모델 | 학습된 가중치·구조·토크나이저의 묶음 | README의 Hugging Face 모델 링크 |
| 변환 도구 | 모델의 이름·배열·배율을 실행 형식으로 옮기는 프로그램 | utils/convert-hf-to-gguf-bitnet.py |
| 추론 엔진 | 모델을 불러와 연산을 실행하는 프로그램 | llama.cpp 기반 실행 파일과 BitNet 커널 |
| 사용자 진입점 | 엔진에 모델 경로·프롬프트·스레드 수를 전달하는 도구 | run_inference.py, run_inference_server.py |
이 자료의 중심 질문은 “어떤 답을 생성하는가”보다 “그 답을 계산하는 실행 경로가 어떻게 구성되는가”입니다. Python 문법을 조금 알고 배열·반복문을 읽을 수 있다면 시작할 수 있습니다.
큰 모델은 매 토큰을 만들 때 많은 가중치를 읽습니다. 특히 한 문장을 한 토큰씩 생성하는 구간에서는 계산 장치보다 메모리에서 숫자를 가져오는 속도가 병목이 될 수 있습니다. 가중치를 더 작게 저장하면 전송량을 줄이고 캐시에 더 많은 데이터를 담을 여지가 생깁니다. BitNet은 이런 효과를 삼진 가중치에 맞춘 커널로 활용합니다.
낮은 비트 수 자체보다 중요한 것은 압축을 푸는 비용까지 포함한 전체 실행 시간을 줄이는 일입니다. 그래서 이 저장소는 모델 연구와 시스템 프로그래밍을 연결해서 공부하기에 좋습니다.
선정 근거는 2026-10-23 기준 GitHub Trending C++ Monthly 9위입니다. 위 세 가지는 코드와 공개 설명에서 읽을 수 있는 관심 요인이며, 순위를 만든 원인을 통계적으로 증명한 분석은 아닙니다.
| 대상 | 주된 역할 | BitNet의 위치와 선택 기준 |
|---|---|---|
| llama.cpp | 여러 모델·양자화 형식을 다루는 추론 기반 | BitNet은 이를 기반으로 삼진 모델용 경로를 확장합니다. 서로 완전히 독립적인 경쟁 제품으로 보기 어렵습니다. |
| T-MAC | 룩업 테이블 방식의 저비트 연산 | README는 커널의 방법론적 기반으로 T-MAC을 명시하며, 삼진을 넘어 일반 저비트 모델에는 T-MAC을 권합니다. |
| 일반 F16/BF16 실행 | 더 높은 정밀도로 가중치와 연산을 처리 | BitNet은 삼진 선형층의 저장·대역폭 비용을 줄입니다. 모델 품질과 속도는 별도 항목으로 비교해야 합니다. |
| 학습 프레임워크 | 역전파로 모델의 가중치를 학습 | 이 저장소의 중심은 추론입니다. 임의의 모델을 같은 품질의 BitNet 모델로 학습시키는 전체 파이프라인은 아닙니다. |
README는 ARM CPU에서 1.37~5.07배, x86 CPU에서 2.37~6.17배의 속도 향상과 에너지 절감을 주장합니다. 같은 글에는 100B 모델의 단일 CPU 실행에 대한 설명도 있습니다. 모두 README와 연결된 기술 보고서의 실험 주장이며, 모든 노트북과 모델에 적용되는 보장값은 아닙니다.
docs/bitnet-embeddings-i2s-guide.md의 임베딩 실험은 조건이 더 구체적입니다.
비교 대상은 F16 교사 모델과 I2_S BitNet 모델입니다.
Intel Xeon Platinum 8573C, 8스레드에서 0.6B 모델의 pp128 처리량을
F16 382.15 t/s, I2_S 870.90 t/s로 기록합니다.
pp4096에서는 각각 236.76, 336.32 t/s로, 배율이 2.28배에서 1.42배로 줄어듭니다.
식당에서 재료 운반을 두 배 빠르게 만들어도 굽는 시간이 길어지면 전체 음식은 두 배 빨리 나오지 않습니다. 프롬프트가 길어져 Attention 비중이 커지는 상황도 비슷합니다. 한 부분의 최적화가 전체 시간을 얼마나 바꾸는지 보려면, 각 부분이 차지하는 시간을 알아야 합니다.
GPU 문서의 종단 간 비교는 BitNet 모델과 Gemma-2-2B/vLLM 조합을 비교합니다. 모델과 실행 체계가 함께 달라지므로 그 배율을 “CUDA 커널 하나의 순수한 효과”라고 해석하면 안 됩니다. 커널 단독 지연시간 표와 전체 생성 지연시간 표를 나눠 읽는 연습이 필요합니다.
| 계층 | 기술과 실제 파일 | 책임 |
|---|---|---|
| CPU 실행 | C/C++, llama.cpp·GGML, src/ggml-bitnet-mad.cpp | I2_S 가중치와 정수 활성값의 내적, AVX2·NEON 분기 |
| CPU 룩업 경로 | src/ggml-bitnet-lut.cpp, include/bitnet-lut-kernels.h | TL1·TL2 형식, 테이블 전처리와 계산 연결 |
| 모델 준비 | Python, NumPy·PyTorch·GGUF 관련 import, utils/ | 체크포인트 읽기, 텐서 이름 매핑, 비트 패킹, 메타데이터 기록 |
| GPU 실행 | PyTorch, xformers, CUDA, gpu/model.py | Attention·캐시 처리와 전용 W2A8 GEMV 호출 |
| 사용자 접점 | argparse, subprocess, 두 실행 래퍼 | 명령행 옵션을 llama-cli·llama-server에 전달 |
웹 서비스의 계층 이름에 대응시키면 “백엔드”는 모델을 계산하는 엔진이고,
“프론트엔드”는 우선 CLI입니다.
최상위에는 package.json과 독립적인 웹 프론트엔드 프로젝트가 없습니다.
서버 실행 파일은 CMake에서 켜지만, 서버의 HTTP 처리나 UI 구현은 의존하는 llama.cpp 쪽 영역입니다.
| 항목 | 확인한 버전·설정 | 근거와 해석 |
|---|---|---|
| Python | >=3.10 | 루트 README의 CPU 설치 요구사항. 별도 Python 잠금 파일은 없습니다. |
| CMake | README >=3.22, 루트 선언 3.14 | cmake_minimum_required(VERSION 3.14). 최소 선언과 문서 권장 환경을 구분합니다. |
| 컴파일러 | README clang>=18 | setup_env.py는 clang·clang++를 지정합니다. src/CMakeLists.txt는 Clang 또는 GCC 계열을 허용하며 최소 버전을 검사하지 않습니다. |
| 언어 표준 | CPU C11, GPU C++17 | 루트는 CMAKE_C_STANDARD 11. GPU compile.sh에는 -std=c++17. 루트에서 CPU C++ 표준의 숫자는 직접 지정하지 않습니다. |
| GPU PyTorch | torch>=2.2.0 | gpu/requirements.txt의 하한. 특정 재현 버전으로 고정된 값은 아닙니다. |
| GPU xformers | xformers>=0.0.22 | 같은 파일의 하한. transformers, sentencepiece 등은 버전이 없습니다. |
| llama.cpp | 390c307752ab78fd8189f359d6954c9ba1be74af | 커밋 0b341e5가 기록한 서브모듈 커밋. 브랜치 이름보다 재현 기준으로 명확합니다. |
루트 requirements.txt는 자체 버전 목록 대신
3rdparty/llama.cpp/requirements/requirements-convert_hf_to_gguf.txt 등을
-r로 참조합니다.
의존성 파일이 다른 파일을 가리키면 그 참조 끝까지 읽어야 실제 설치 환경을 알 수 있습니다.
루트 요구사항 파일만으로는 CPU용 전이 의존성의 정확한 버전을 확정할 수 없습니다.
별도로 기록된 서브모듈 커밋의 요구사항 파일까지 확인해야 합니다.
CMakeLists.txt는 기본 빌드 형식을 조건부로 Release로 설정하고,
실행 파일을 build/bin에 모읍니다.
find_package(Threads REQUIRED)로 스레드 라이브러리를 찾고,
LLAMA_BUILD_SERVER를 ON으로 강제합니다.
# CMakeLists.txt에서 발췌한 기본 설정
option(BITNET_ARM_TL1 "bitnet.cpp: use tl1 on arm platform" OFF)
option(BITNET_X86_TL2 "bitnet.cpp: use tl2 on x86 platform" OFF)
set(CMAKE_C_STANDARD 11)
set(LLAMA_BUILD_SERVER ON CACHE BOOL "Build llama.cpp server" FORCE)
add_subdirectory(3rdparty/llama.cpp)
두 룩업 옵션은 기본값이 OFF이고, setup_env.py의
COMPILER_EXTRA_ARGS도 해당 아키텍처의 룩업 옵션을 OFF로 전달합니다.
따라서 파일이 존재한다는 사실만으로 기본 실행이 TL1·TL2라고 결론 내릴 수 없습니다.
.gitmodules의 URL은 isHuangXin/llama.cpp 포크이며,
브랜치는 release-bitnet-embedding-0.6b-270m입니다.
일반 llama.cpp 최신판과 같은 의존성이라고 가정하면 안 됩니다.
최상위 Dockerfile, .github/workflows/, pyproject.toml,
Cargo.toml, go.mod는 이 커밋의 추적 파일에 없습니다.
여기서 확인되는 배포 준비의 중심은 CMake와 Python 스크립트입니다.
run_inference.py → llama-cli → GGML 연산 → BitNet 커널 순서로 읽으세요.
이후 변환 도구와 코드 생성기를 거꾸로 따라가면 “실행 전에 무엇을 준비해야 하는가”가 연결됩니다.
도식의 llama.cpp·GGML 내부 연결은 루트 CMake, 헤더의 연산 인터페이스, 문서가 가리키는 구조를 요약한 것입니다. 서브모듈 내부의 개별 호출 순서까지 펼친 도식은 아닙니다. 바깥쪽 Python 코드와 이 저장소의 커널 구현은 아래에서 함수 단위로 살펴봅니다.
run_inference.py의 run_inference()는 신경망을 직접 계산하지 않습니다.
운영체제에 따라 실행 파일 경로를 고르고, 명령을 리스트로 만든 뒤
run_command()를 통해 subprocess.run(..., check=True)로 전달합니다.
# run_inference.py의 명령 구성 중 핵심 부분
command = [
f'{main_path}',
'-m', args.model,
'-n', str(args.n_predict),
'-t', str(args.threads),
'-p', args.prompt,
'-ngl', '0',
'-c', str(args.ctx_size),
'--temp', str(args.temperature),
]
if args.conversation:
command.append('-cnv')
-ngl 0은 GPU로 넘기는 레이어 수를 0으로 지정합니다.
이 래퍼의 기본 실행 의도는 CPU 추론입니다.
GPU 실행은 gpu/ 아래의 별도 코드로 읽어야 합니다.
Windows에서는 build/bin/Release/llama-cli.exe를 먼저 찾습니다.
이 설계에서 배울 점은 준비·인자 처리와 계산 엔진의 책임 분리입니다.
명령을 문자열 하나로 합치지 않고 리스트로 전달하며, 기본 shell=False를 유지합니다.
공백이 있는 프롬프트도 하나의 인자로 전달할 수 있습니다.
다만 자식 프로세스 오류는 종료 코드 1로 단순화되므로, 더 풍부한 오류 구조를 제공하는 래퍼는 아닙니다.
서버 쪽 run_server()는 같은 방식으로 llama-server를 실행하며
-cb를 붙여 연속 배칭을 켭니다.
기본 주소는 127.0.0.1, 포트는 8080입니다.
Python에 별도의 HTTP 라우터를 구현한 구조는 아닙니다.
src/ggml-bitnet-mad.cpp의 quantize_i2_s()는
거의 0인 값을 코드 1로, 양수를 2로, 음수를 0으로 바꿉니다.
0 판정의 기준은 fabs(src[i]) < 1e-6입니다.
QK_I2_S는 x86 분기에서 128, ARM NEON 분기에서 64입니다.
기본 x86 ACT_PARALLEL 경로에서는 128개 값을 4×32로 나눕니다. 같은 열의 네 코드를 한 바이트에 묶어, 나중에 32바이트 SIMD 로드로 꺼내기 쉬운 배치를 만듭니다. “서로 이웃한 네 숫자를 무조건 한 바이트로 묶는다”는 설명만으로는 실제 배열 순서를 이해할 수 없습니다.
# 실제 배치를 이해하기 위한 인덱스 예시
입력 128개를 4행 × 32열로 해석한다.
첫 바이트 = 코드[0], 코드[32], 코드[64], 코드[96]
둘째 바이트 = 코드[1], 코드[33], 코드[65], 코드[97]
# quantize_i2_s()의 x86 ACT_PARALLEL 분기에서 발췌
int group_idx = j / 32;
int group_pos = j % 32;
uint8_t temp = (q8[i * QK_I2_S + j] << (6 - 2 * group_idx));
i2_weight[i * 32 + group_pos] |= temp;
이어지는 ggml_vec_dot_i2_i8_s_Nx1()는
_mm256_loadu_si256로 묶음을 읽고,
시프트와 0x03 마스크로 코드를 꺼내며,
_mm256_maddubs_epi16 등으로 정수 내적을 누산합니다.
압축 코드에 대한 정수 누산과 모델의 최종 실수 출력은 같은 단계가 아닙니다.
배율과 코드의 영점 규약까지 맞아야 최종 의미가 보존됩니다.
ggml_vec_dot_i2_i8_s()는 nrc가
PARALLEL_SIZE로 나누어떨어질 때 병렬 묶음 함수를 선택하고,
나머지 경우 _1x1 경로를 사용합니다.
이는 입력 조건에 맞는 계산 전략을 고르는 작은 디스패처입니다.
// src/ggml-bitnet-mad.cpp의 분기를 축약한 설명용 코드
if (nrc % PARALLEL_SIZE == 0) {
#if defined(ACT_PARALLEL)
ggml_vec_dot_i2_i8_s_Nx1(/* 인자 생략 */);
#else
ggml_vec_dot_i2_i8_s_1xN(/* 인자 생략 */);
#endif
} else {
ggml_vec_dot_i2_i8_s_1x1(/* 인자 생략 */);
}
include/gemm-config.h는 ACT_PARALLEL을 정의합니다.
기본 x86 설정은 ROW_BLOCK_SIZE=4, COL_BLOCK_SIZE=128,
PARALLEL_SIZE=4입니다.
_Nx1 내부는 가중치를 한 번 꺼낸 뒤 여러 활성값 열에 재사용합니다.
여기의 병렬 묶음 수는 운영체제 스레드 수와 다른 설정입니다.
빠른 경로를 읽을 때는 그 경로에 들어가는 조건과 조건을 만족하지 못했을 때의 동작을 함께 읽어야 합니다.
같은 질문을 LUT 경로에도 적용할 수 있습니다.
ARM의 ggml_bitnet_can_mul_mat()는 지원 가중치 형식, F32 입력·출력,
src1->ne[1] <= 1을 검사합니다. x86 TL2 쪽에는 마지막 조건이 없습니다.
utils/codegen_tl2.py의 ModelShapeDict는 모델별 행렬 모양을 저장합니다.
gen_tbl_impl(), gen_top_api(), gen_transform_code()로
코드 문자열을 구성하고 include/bitnet-lut-kernels.h와
include/kernel_config.ini에 기록합니다.
반복해서 실행할 연산의 모양을 미리 알고, 그 모양에 맞는 코드를 만드는 설계입니다.
TL2의 get_three_k_two_k(K, bk)는
three_k = (K // bk) * bk와 two_k = K - three_k로
주 처리 구간과 남는 구간을 나눕니다.
예를 들어 K=3200, bk=96이면 three_k=3168, two_k=32입니다.
생성기는 M % BM == 0, (K % BK) % 32 == 0,
bm in [32]를 검사합니다.
검사는 일부 코드 문자열 생성 뒤에 위치하므로, 모든 잘못된 인자가 친절한 사전 오류로 걸러진다고 가정할 수는 없습니다.
모델 모양을 바꾸면 생성 코드·설정·변환 형식을 함께 검토해야 합니다.
모든 크기의 종이를 자르는 만능 재단기 대신, 자주 쓰는 규격 전용 칼틀을 미리 만드는 방식입니다. 전용 틀은 빠르지만 다른 규격의 종이를 넣었을 때 어떻게 처리할지도 설계해야 합니다. 이 저장소의 코드 생성기와 GPU의 모양별 분기는 같은 질문을 서로 다른 방법으로 다룹니다.
LUT의 작업 순서도 중요합니다.
ARM ggml_bitnet_mul_mat()는 ith == 0에서 테이블 전처리를 하고,
여러 스레드라면 ggml_barrier(params->threadpool)를 거친 뒤 계산 함수를 호출합니다.
공유 테이블을 다 만들기 전에 다른 스레드가 읽지 않도록 순서를 맞추는 것입니다.
gpu/generate.py의 FastGen.build()는
use_kernel=False인 prefill 모델과 True인 decode 모델을 따로 만들고,
model_state_fp16.pt·model_state_int2.pt를 각각 불러옵니다.
입력을 한꺼번에 처리할 때와 한 토큰을 만들 때 유리한 계산이 다르기 때문입니다.
gpu/model.py의 BitLinearKernel.quant_input()는 토큰별 최대 절댓값으로
s = 127 / max(max(abs(x)), 1e-5)를 구하고, 반올림·범위 제한 뒤 int8로 바꿉니다.
bitnet_int8xint2_linear()는 CUDA 스트림과 텐서 포인터를
ctypes로 libbitnet.so에 전달합니다.
gpu/bitnet_kernels/bitnet_kernels.cu의 bitlinear_int8xint2()는
M == 1인 특정 N·K 조합만 분기 처리합니다.
미지원 모양에서는 메시지를 출력하며 범용 계산으로 대체하지 않습니다.
실제 커널 ladder_int8xint2_kernel은 헤더에 있고 __dp4a를 사용합니다.
따라서 “GPU 지원”을 임의 크기의 배치·모든 모델에 대한 범용 지원으로 확대 해석하면 안 됩니다.
| 파일·폴더 | 읽을 때 던질 질문 |
|---|---|
src/ | 이미 준비된 숫자를 CPU가 어떻게 계산하는가? |
utils/ | 계산 전에 숫자·이름·배치를 어떻게 맞추는가? |
include/ | 계산 함수가 외부에 내놓는 약속과 컴파일 설정은 무엇인가? |
preset_kernels/ | 특정 모델 모양에 맞춘 결과를 어떻게 재사용하는가? |
3rdparty/ | 이 저장소가 직접 구현하지 않고 가져다 쓰는 실행 기반은 무엇인가? |
utils/convert-bitnet-embedding-to-gguf.py는 임베딩용 독립 변환기이고,
utils/convert-hf-to-gguf-bitnet.py는 다른 모델 경로도 다루는 변환기입니다.
파일 이름이 비슷해도 지원 모델, 배율 처리, 텐서 매핑은 각각 확인해야 합니다.
| 언어·종류 | 파일 수 | 물리적 줄 수 |
|---|---|---|
Python .py | 26 | 11,104 |
C++ .cpp | 2 | 1,248 |
헤더 .h | 10 | 7,154 |
CUDA .cu | 1 | 37 |
Shell .sh | 3 | 727 |
| CMakeLists.txt | 2 | 88 |
| 합계 | 44 | 20,358 |
커밋 0b341e5 기준의 물리적 줄 수이며, 빈 줄·주석·개행 없는 마지막 줄을 포함하고 서브모듈 본문은 제외합니다.
헤더 중 preset_kernels/가 5,813줄,
include/bitnet-lut-kernels.h가 1,171줄입니다.
CUDA 본체 37줄만 보면 작아 보이지만, 실제 계산 템플릿은 83줄짜리 헤더에도 있습니다.
파일 확장자별 줄 수를 곧바로 구현 난도나 사람이 직접 작성한 분량으로 해석하지 마세요.
argparse 선언은 사용자가 바꿀 수 있는 설정의 목록입니다.
run_inference.py에서 --n-predict는 기본 128,
--threads는 2, --ctx-size는 2048,
--temperature는 0.8입니다.
이 값들이 문자열로 바뀌어 자식 프로세스에 전달되는 곳까지 연결해 보세요.
작은 실습: 옵션 이름, Python 변수명, 실행 파일에 전달하는 옵션을 세 열로 정리합니다.
--conversation처럼 값 없이 켜는 옵션과 --threads처럼 값이 필요한 옵션을 비교하세요.
CLI 설계와 프로그램 경계에서의 타입 변환을 배울 수 있습니다.
utils/convert-bitnet-embedding-to-gguf.py의 quantize_to_i2_s()는
abs_mean = max(mean(abs(w)), 1e-5)를 계산하고,
round(w / abs_mean)을 −1~1로 제한합니다.
저장되는 배율은 abs_mean이므로 복원 개념은 q * abs_mean입니다.
# 개념 확인용 숫자 예시: 실제 모델 출력이 아니다
w = [-0.6, -0.2, 0.2, 0.6]
mean(abs(w)) = 0.4
w / 0.4 = [-1.5, -0.5, 0.5, 1.5]
# 반올림 규칙까지 확인해야 한다. 단순히 소수점을 버리는 연산이 아니다.
q = clip(round(w / 0.4), -1, 1)
복원값 = q * 0.4
반면 C++ quantize_i2_s()는 최대 절댓값과 부호·0 판정을 사용합니다.
두 함수의 이름이 비슷하다고 같은 임의 실수 입력에 대해 같은 결과를 낸다고 가정하면 안 됩니다.
삼진화가 이미 적용된 가중치를 포장하는 단계인지, 학습된 실수 가중치에서 삼진값을 만드는 단계인지 확인하세요.
변환기의 정확성은 파일을 성공적으로 쓰는 데서 끝나지 않고 숫자·배율·텐서 의미를 함께 보존하는 데서 결정됩니다.
일반 변환기의 BitnetModel.write_tensors()는 이미 양자화된 모델의
weight_scale을 별도로 모아 처리합니다.
이미 삼진인 값을 다시 양자화하면 배율 규약을 망가뜨릴 수 있기 때문입니다.
임베딩 변환기의 set_gguf_parameters()는 설정의 head_dim을 우선 사용하고,
없을 때만 hidden_size // num_attention_heads를 사용합니다.
문서의 0.6B 예시는 실제 head_dim=128인데 단순 나눗셈 결과는 64입니다.
배열의 차원 규약을 잘못 적으면 가중치 숫자가 맞아도 모델 계산이 달라집니다.
build_tensor_name_map()은 학습 측 이름을 GGUF 이름으로 바꿉니다.
예를 들어 layers.{i}.self_attn.q_proj.norm.weight는
blk.{i}.attn_q_norm_in.weight로 연결됩니다.
이 정규화는 선형층 앞에 있으므로 기존 모델의 비슷한 이름에 대충 합칠 수 없습니다.
Gemma3 고유 정규화는 (1+w) 규약을 맞추려고 변환 시 w+1을 적용합니다.
그러나 BitLinear의 _norm_in.weight는 이 보정에서 제외합니다.
작은 실습: “이름 매핑”, “숫자 변환”, “차원 기록”을 서로 다른 책임으로 표시해 보세요.
스레드 수 -t를 늘리는 것은 일을 여러 작업자에게 나누는 것입니다.
PARALLEL_SIZE는 한 커널 안에서 묶어서 처리하는 활성값·가중치 개수에 관여합니다.
캐시와 레지스터가 부족하면 묶음을 키워도 성능이 좋아지지 않을 수 있습니다.
작은 실습: include/gemm-config.h에서 x86, ARM DOTPROD,
일반 ARM NEON의 설정을 비교합니다.
기본 활성값 병렬 설정의 PARALLEL_SIZE는 각각 4, 8, 4입니다.
같은 소스가 전처리 매크로에 따라 다른 프로그램이 되는 과정을 익힐 수 있습니다.
utils/e2e_benchmark.py의 run_benchmark()는
llama-bench에 -ngl 0, -b 1, -r 5를 전달합니다.
기본 프롬프트 토큰 수는 512, 생성 토큰 수는 128입니다.
같은 “tokens/s”라도 프롬프트 처리와 토큰 생성은 서로 다른 일을 측정합니다.
또한 이 파일의 run_command()는 log_step이 없는 분기에서
성공 후에도 sys.exit(1)에 도달합니다.
벤치마크 본체의 출력과 래퍼의 종료 상태를 따로 봐야 하는 이유입니다.
소스에서 보이는 제어 흐름이며, 실제 실행 결과를 측정한 주장은 아닙니다.
작은 실습: 모델·형식·CPU·스레드·pp·tg·반복 수·평균·편차를 담는 결과 표를 설계하세요. 숫자 하나를 얻는 것보다 다른 사람이 같은 조건을 이해할 수 있게 기록하는 것이 목표입니다.
입력은 몇 개이고 출력은 몇 개인지, 한 번 읽은 가중치를 몇 번 쓰는지, 반복문의 끝에서 포인터가 얼마나 이동하는지부터 보세요. 그 흐름이 잡힌 뒤 개별 명령을 조사하면 긴 커널도 읽을 수 있는 크기로 나뉩니다.
| 목적 | 필요 환경 | 판단 근거 |
|---|---|---|
| 소스 학습 | 텍스트 편집기, 파일 검색 도구 | 모델 다운로드나 GPU가 없어도 래퍼·패킹·디스패치·변환기를 읽을 수 있습니다. |
| CPU 추론 | x86-64 AVX2 또는 ARM NEON 계열 | src/README.md의 지원 표와 커널 분기. ARM DOTPROD 전용 경로도 있습니다. |
| CPU 빌드 | README 기준 Python ≥3.10, CMake ≥3.22, Clang ≥18 | Windows는 Visual Studio 2022의 C++·CMake·Clang 도구를 안내합니다. Conda는 권장 사항입니다. |
| GPU 실험 | NVIDIA CUDA 개발 환경과 호환 PyTorch·xformers | compile.sh는 compute_80을 지정합니다. 문서 벤치마크 장비는 A100 40GB입니다. |
| GPU Python | 문서 예시 python<3.13 | gpu/README.md의 환경 생성 예시. CUDA 도구 모음의 정확한 버전은 고정하지 않습니다. |
| 모델 저장 공간 | 모델 형식과 변환 중간 파일을 함께 수용 | I2_S 준비 경로는 F32 GGUF를 먼저 만들 수 있습니다. 최종 파일 크기만으로 디스크 여유를 잡으면 부족할 수 있습니다. |
# 모든 가중치가 같은 비트 수라는 단순화된 하한 계산
가중치 저장량 ≈ 파라미터 수 × 비트 수 / 8
2.4B × 2비트 / 8 = 0.6 GB
2.4B × 16비트 / 8 = 4.8 GB
# 실제 실행에는 다음 항목이 추가된다
전체 메모리 ≈ 가중치 + 고정밀 텐서 + 배율·정렬 공간
+ KV 캐시 + 활성값·작업 버퍼 + 실행 환경
여기서 GB는 10억 바이트입니다. README의 BitNet-b1.58-2B-4T는 실제 파라미터 수를 2.4B로 설명합니다. 모델 이름의 “2B”와 계산에 넣는 정확한 수를 구분하세요.
2비트 가중치의 이론적 저장량은 모델 파일 크기나 실제 실행 메모리의 보장값이 아닙니다. 임베딩 변환기는 I2_S 출력에서 토큰 임베딩과 정규화 텐서를 F16으로 남깁니다. 문서도 0.6B I2_S 출력 예시를 약 699 MiB로 적어, 단순한 0.6B×2비트 계산과 차이가 납니다.
CPU 최소 RAM이나 모든 장치의 VRAM 하한을 이 소스만으로 확정할 수는 없습니다. 특히 GPU 코드는 prefill·decode 모델을 따로 보유하므로 압축 가중치 하나의 크기로 VRAM을 계산하면 안 됩니다. 하드웨어를 고를 때는 모델 파일, 문맥 길이, 동시 요청, 실행 경로를 한 세트로 결정해야 합니다.
컴파일 설정도 호환성에 영향을 줍니다.
문서의 GGML_NATIVE=ON은 빌드 장비의 명령 집합을 활용하는 설정입니다.
다른 CPU에 같은 바이너리를 복사하려면 명령 집합 호환성을 별도로 검토해야 합니다.
GPU의 compute_80 역시 “CUDA라면 어디서나 같은 조건”이라는 뜻은 아닙니다.
아래 명령은 저장소 루트에서 파일을 읽는 예시입니다. 모델 실행이나 설치 없이, 코드 읽기와 종이 위 계산만으로 결과를 만들 수 있습니다. 실습을 마쳤다는 기준은 코드를 훑었다는 감상이 아니라 입력·분기·출력을 근거와 함께 설명할 수 있다는 것입니다.
run_inference.py에서 -p, -t, -cnv를 찾고,
argparse → args → 명령 리스트 → 자식 프로세스의 네 단계를 적습니다.
rg -n 'add_argument|command|subprocess.run' run_inference.py
제출물: 옵션 대응표와 가상의 최종 인자 리스트.
프롬프트는 배열의 원리를 설명해 줘, 스레드는 4, 대화 모드는 켠 것으로 가정합니다.
완료 기준: 한글 프롬프트가 하나의 인자로 남고, -ngl 0과
-cnv가 포함된 이유를 설명합니다. 프롬프트를 공백마다 나누지 마세요.
가중치 [−1, 0, +1, 0]의 코드 [0, 1, 2, 1]을
높은 비트부터 한 바이트에 넣습니다. x86 I2_S 블록에서 이 네 값은
인덱스 [0, 32, 64, 96]에 놓였다고 가정합니다.
# 독립적인 비트 연산 예시. 저장소 함수를 호출하지 않는다.
packed = (0 << 6) | (1 << 4) | (2 << 2) | 1
codes = [(packed >> shift) & 3 for shift in (6, 4, 2, 0)]
weights = [c - 1 for c in codes]
제출물: 2진수·16진수·10진수 표현과 복원 과정.
완료 기준: 00011001, 0x19, 25를 얻고
원래 네 값을 복원합니다. 코드 3이 이 대응표에서 쓰이지 않는 이유도 적습니다.
PARALLEL_SIZE=4, ACT_PARALLEL이 켜진 x86 환경을 가정하고
nrc=1, 4, 6, 8에서 선택되는 함수를 적습니다.
rg -n 'void ggml_vec_dot_i2_i8_s\(|nrc %|ACT_PARALLEL' src/ggml-bitnet-mad.cpp
cat include/gemm-config.h
제출물: 네 입력의 분기표와 가중치 재사용 그림.
완료 기준: 1·6은 _1x1, 4·8은 _Nx1임을 설명합니다.
이 디스패처가 6을 자동으로 4+2로 나누지 않는다는 점까지 확인하세요.
생각할 문제: 나머지 처리 경로를 더 세밀하게 나누면 얻을 이익과 추가되는 코드 복잡도는 무엇일까요? 실행하지 않고 가설을 적어도 좋습니다.
set_gguf_parameters()에서 head_dim과 pooling을 추적합니다.
설정의 head_dim이 있을 때·없을 때,
pooling 설정 파일이 있을 때·없을 때의 결과를 각각 적으세요.
rg -n 'head_dim|pooling_type|pooling_mode|defaulting' utils/convert-bitnet-embedding-to-gguf.py
제출물: 경우별 메타데이터 표와 문서 설명의 대조 메모.
완료 기준: 문서는 last-token pooling을 설명하지만,
설정이 없으면 변환기는 MEAN을 기본값으로 쓴다는 차이를 찾아냅니다.
추가 관찰: 명령이 성공해도 모델 의미가 달라질 수 있는 사례입니다. 파일 존재 여부 검사와 의미 검증이 왜 별개인지 설명하세요.
bitlinear_int8xint2()의 모든 분기를 읽고 지원되는 M·N·K를 표로 옮깁니다.
목록 밖의 모양이 들어오면 Python 호출자까지 어떤 상태가 이어지는지 추적하세요.
cat gpu/bitnet_kernels/bitnet_kernels.cu
rg -n 'torch.zeros|bitlinear_int8xint2|return ret' gpu/model.py
제출물: 지원표, 미지원 입력의 처리 설계, 정확성·성능 검증 계획. 코드를 고치는 대신 명시적 오류를 반환할 위치와 범용 대체 경로의 필요성을 제안합니다.
완료 기준: 현재 미지원 모양에서는 CUDA 계산 없이 메시지만 출력하고, 호출자가 미리 0으로 만든 출력 텐서를 그대로 돌려줄 수 있음을 설명합니다. 향후 검증에는 동일 입력·동일 가중치의 기준 출력 비교, 최대 오차, 워밍업, CUDA 동기화, 단독 커널 시간과 전체 생성 시간의 구분을 포함합니다.
| 주차 | 배울 내용과 읽을 파일 | 확인할 결과물 |
|---|---|---|
| 1주차 | 토큰·벡터·행렬의 기초, Python CLI.run_inference.py, run_inference_server.py | 옵션 대응표, CPU 실행 경로 그림, CLI와 서버의 기본값 비교 |
| 2주차 | 정수·부동소수점·배율·양자화.quantize_to_i2_s(), BitnetModel.weight_quant() | 작은 배열의 양자화·복원 계산, 반올림·범위 제한의 역할 설명 |
| 3주차 | C/C++ 포인터·비트 연산·메모리 배치.src/ggml-bitnet-mad.cpp | 128개 코드가 32바이트에 들어가는 인덱스 표, SIMD 로드 단위 그림 |
| 4주차 | 캐시·타일링·데이터 재사용·동기화.include/gemm-config.h, src/ggml-bitnet-lut.cpp | 분기표, 테이블 생성과 barrier의 순서도, 스레드 수와 묶음 크기 비교 |
| 5주차 | 코드 생성·GGUF 메타데이터·모델 호환성.utils/codegen_tl2.py, 임베딩 변환기 | 타일 조건 계산, 텐서 이름 매핑표, pooling·head_dim 검증 목록 |
| 6주차 | GPU 실행 경계와 성능 평가 설계.gpu/model.py, gpu/bitnet_kernels/, utils/e2e_benchmark.py | 지원 모양 표, 정확성 기준, pp·tg를 구분한 실험 계획서 |
주당 3~5시간을 가정한 학습 제안입니다. CUDA가 처음이라면 6주차를 두 주로 나누어도 좋습니다. 먼저 같은 연산을 손으로 설명할 수 있게 된 뒤, 벡터 명령이나 GPU 스레드 배치를 공부하세요. 논문은 2주차에 b1.58의 표현을, 4~5주차에 bitnet.cpp의 시스템 설계를 연결해서 읽으면 좋습니다.
새 최적화를 배우면 먼저 무엇을 줄였는지 쓰고, 다음으로 무엇이 그대로 남는지 측정 항목으로 정리하세요. 예를 들어 압축은 가중치 전송을 줄일 수 있지만 Attention·KV 캐시·프로세스 준비 비용까지 없애지는 않습니다. 이 구분을 익히면 다른 추론 엔진의 성능 주장도 더 정확하게 읽을 수 있습니다.
입력 토큰 → 활성값 → 양자화 → 패킹된 가중치 읽기 → 정수 누산 → 배율 복원 → 출력의 흐름을 그립니다. 각 화살표 옆에 관련 파일·함수 또는 아직 확인할 의존 영역을 적으세요. 정확히 아는 부분과 더 읽어야 할 부분을 구분하는 능력도 소스 분석의 결과입니다.
| 용어 | 쉬운 설명 |
|---|---|
| 추론 | 이미 학습한 가중치를 사용해 입력의 출력값을 계산하는 일입니다. |
| 토큰 | 모델이 다루는 텍스트 조각입니다. 한국어의 글자 수·단어 수와 정확히 일치하지 않습니다. |
| 가중치 | 학습 과정에서 얻은 모델의 숫자입니다. 선형층에서는 행렬로 모여 계산에 사용됩니다. |
| 활성값 | 입력이 모델을 지나면서 생기는 중간 숫자입니다. 가중치와 정밀도가 다를 수 있습니다. |
| 양자화 | 넓은 범위의 숫자를 더 적은 표현 단계로 바꾸는 것입니다. 배율·반올림·범위 제한을 함께 봐야 합니다. |
| 삼진 가중치 | −1, 0, +1의 세 상태로 표현하는 가중치입니다. 실제 값에는 공통 배율이 붙을 수 있습니다. |
| b1.58 | 세 상태의 정보량인 log₂(3)에서 온 이름입니다. 파일의 모든 숫자가 정확히 1.58비트라는 뜻은 아닙니다. |
| I2_S | 이 저장소에서 사용하는 2비트 삼진 가중치 형식입니다. 코드뿐 아니라 배율과 배열 순서가 필요합니다. |
| W2A8 | 가중치를 2비트, 활성값을 8비트로 표현하는 계산 구성을 가리킵니다. |
| GGML / GGUF | GGML은 텐서 계산 기반이고, GGUF는 모델 텐서와 메타데이터를 담는 파일 형식입니다. |
| GEMV / GEMM | 각각 행렬×벡터, 행렬×행렬 곱입니다. 한 토큰 생성과 여러 토큰 처리는 계산 모양이 달라집니다. |
| prefill / decode | prefill은 입력 토큰들을 처리하는 단계, decode는 다음 토큰을 순차 생성하는 단계입니다. |
| pp / tg | 벤치마크의 프롬프트 처리와 토큰 생성을 뜻합니다. pp128과 tg128은 서로 다른 작업입니다. |
| SIMD | 명령 하나로 여러 숫자를 처리하는 방식입니다. AVX2·NEON은 관련 명령 집합입니다. |
| 타일링 | 큰 행렬을 작은 블록으로 나누어 캐시와 레지스터에서 데이터를 재사용하는 기법입니다. |
| LUT / TL1·TL2 | LUT는 미리 계산한 값을 찾는 표입니다. TL1·TL2는 이 저장소의 전용 룩업 기반 형식·계산 경로입니다. |
| 디스패치 | 입력 모양·자료형·장치 조건에 따라 적절한 계산 함수를 선택하는 과정입니다. |
| barrier | 여러 스레드가 특정 지점까지 도착할 때까지 기다려 공유 작업의 순서를 맞추는 장치입니다. |
| KV 캐시 | 이전 토큰의 Attention용 Key·Value를 저장해 반복 계산을 줄이는 메모리입니다. |
| pooling | 여러 토큰의 벡터에서 문장 대표 벡터를 고르는 방법입니다. 평균과 마지막 토큰 선택은 결과가 다릅니다. |
| FFI / ctypes | 다른 언어의 함수를 호출하는 경계입니다. GPU 코드에서는 Python이 포인터를 넘겨 네이티브 함수를 부릅니다. |
| dp4a | 네 쌍의 8비트 정수 곱을 더해 정수 누산값에 반영하는 CUDA 명령입니다. |
| 서브모듈 | 다른 Git 저장소의 특정 커밋을 의존성으로 기록하는 방식입니다. 폴더 이름만 있다고 내용이 모두 있는 것은 아닙니다. |
| 코드 생성 | 다른 프로그램이 소스 코드를 만드는 기법입니다. 여기서는 Python이 모델 모양에 맞는 C++ 헤더를 만듭니다. |
| 항목 | 상태 |
|---|---|
| 저장소 | microsoft/BitNet · 공식 1-bit LLM 추론 프레임워크 |
| 트렌딩 선정 | GitHub Trending C++ Monthly #9 · 2026-10-23 |
| 스타 / 포크 | 41,228 / 3,838 · 2026-10-23 GitHub 기준 |
| 생성일 | 2024-08-05 |
| 주 언어 | C++ · GitHub 메타데이터 기준 |
| 최신 릴리스 | GitHub Releases 없음. README의 “1.0” 배지와 2024-10-17 공개 소식은 GitHub 릴리스 항목과 별개입니다. |
| 최근 push | 2026-07-27 |
| 최근 커밋 | 0b341e5 · 2026-07-27 · Add VibeASR.cpp release to News (#596) |
| 코드 규모 | 서브모듈 제외 44개 코드·빌드 파일, 20,358줄. Python 11,104줄, C++ 1,248줄, 헤더 7,154줄, CUDA 37줄, Shell 727줄, CMake 88줄. |
| 주요 외부 기반 | llama.cpp 포크의 커밋 390c307. 이 외부 의존성의 코드 규모는 위 줄 수와 별도입니다. |
| 라이선스 | MIT · 루트 LICENSE 기준 |
| 설명·기대 | 커밋 0b341e5에서 보이는 실제 조건 |
|---|---|
| 임베딩을 Q6_K로 양자화 | src/README.md는 --quant-embd를 Q6_K라고 설명하지만, setup_env.py의 I2_S 분기는 --token-embedding-type f16을 전달합니다. |
| last-token pooling | 임베딩 변환기는 설정 파일을 읽고, pooling을 결정하지 못하면 MEAN을 기본값으로 선택합니다. |
| 임베딩 모델의 간편 설치 | SUPPORTED_HF_MODELS에는 임베딩 이름이 있지만 gen_code()에는 해당 이름의 분기가 없습니다. 현재 흐름에서는 모델 준비 전에 NotImplementedError에 도달할 수 있습니다. |
| GPU 계산 지원 | 커널 디스패처는 특정 M=1 모양을 열거합니다. 미지원 모양의 범용 대체 연산은 없습니다. |
| 벤치마크 명령의 성공 | utils/e2e_benchmark.py의 래퍼는 로그 단계가 없는 분기에서 성공 후에도 종료 코드 1을 반환하는 제어 흐름입니다. |
이 엔진은 BitNet 방식에 맞는 모델과 변환 규약을 전제로 합니다. 임의의 일반 모델을 2비트로 저장한다고 원래 품질이 자동으로 유지되지는 않습니다. README의 “무손실” 표현도 이미 학습된 삼진 모델을 실행하는 맥락과 정확성 검증 조건 안에서 읽어야 합니다.
성능 배율은 각 문서의 장비·모델·입력 길이·비교 기준에 따른 결과입니다. 임베딩 가이드의 일부 정확성 표는 평가 설정이 다르다고 명시하므로 열의 숫자 차이를 곧바로 변환 오차로 볼 수 없습니다. 100B CPU 실행 설명 역시 모든 100B 체크포인트의 공개·호환성을 보장하는 지원표가 아닙니다.
서브모듈 내부의 전체 연산 연결과 전이 의존성 버전은 별도 확인 영역입니다. NPU는 README에 향후 지원 계획으로 적혀 있습니다. 배포 판단에는 위의 문서·코드 차이와 CPU 명령 집합, GPU 모양 제약을 함께 반영해야 합니다.