zlib-rs에서 AVX-512 SIMD 인트린식을 활용하면서 CI에서 테스트하기 위해 Miri에 필요한 AVX-512 인트린식 몇 가지를 추가로 구현한 과정을 설명한다.
2025-12-09 작성자: Folkert de Vries zlib-rs simd miri
최근 zlib-rs에서 더 많은 avx512 기능을 활용하기 시작했습니다. avx512 계열의 타깃 기능은 512비트 벡터(256비트 벡터를 쓰는 avx2의 두 배 크기)를 사용하는 SIMD 인트린식을 제공합니다. 이렇게 더 넓은 인트린식은 특정 알고리즘을 극적으로 가속할 수 있습니다.
2023년에 zlib-rs 작업을 처음 시작했을 때는 많은 avx512 타깃 기능이 아직 불안정(stable이 아님)이었고, 개발에 사용할 avx512 하드웨어도 없었습니다. 몇 년이 지난 지금은 프로젝트가 성숙했고 하드웨어도 더 좋아졌으며, Rust 역시 avx512 타깃 기능과 인트린식을 안정화했습니다.
zlib-rs 코드베이스에는 avx512를 사용하면 이득을 볼 것이라 생각하는 부분이 세 군데 있습니다. 이들의 구현은 zlib-ng 구현을 바탕으로 адап트했습니다:
.gz에 사용되는 체크섬입니다.대개 슬라이스와 이터레이터를 활용해 구현을 약간 더 깔끔하게 만들 수 있습니다. 이런 알고리즘을 Rust로 옮기는 작업은 대부분은 straightforward합니다.
실제 하드웨어에서 구현을 마치고 검증까지 했지만, 한 가지 문제가 남았습니다. CI에서 어떻게 테스트할까요? 표준 GitHub CI 러너는 avx2는 지원하지만 avx512는 지원하지 않습니다. 그리고 커스텀 CI 머신을 설정하는 일은 딱히 하고 싶지 않습니다.
CI 하드웨어가 avx512를 지원하지 않지만, 혹시 명령어를 에뮬레이션할 수는 없을까요? CI에서는 최고의 성능이 아니라 동작의 정확성만 중요합니다. 우리는 이미 CI 파이프라인에서 qemu 에뮬레이터를 사용해 예를 들어 s390x-unknown-linux-gnu 타깃에 대한 테스트도 실행하고 있습니다.
불행히도 qemu는 avx512를 지원하지 않습니다. 심지어 간단한 것들조차 지원하지 않는 것 같습니다:
qemu-x86_64: warning: TCG doesn't support requested feature: CPUID.07H:EBX.avx512f [bit 16]
qemu-x86_64: warning: TCG doesn't support requested feature: CPUID.07H:EBX.avx512bw [bit 30]
qemu-x86_64: warning: TCG doesn't support requested feature: CPUID.07H:ECX.vpclmulqdq [bit 10]
이는 사실 어느 정도 합리적입니다. avx512는 수백 개의 새로운 명령어를 포괄하는, 매우 큰 타깃 기능 묶음을 가리키는 우산 용어입니다. 구현하기 그리 즐거운 양이 아닙니다.
rust-lang/stdarch 테스트 스위트에서는 인텔의 에뮬레이터를 사용합니다. 이것은 avx512를 지원하지만, 경험상 꽤 느리기 때문에 특히 사용하고 싶지는 않았습니다.
그래서 이런 생각을 했습니다. 우리는 이 avx512 명령어들을 SIMD 코드에서 사용하고 있고, 그 코드는 어차피 Miri에서 실행하고 싶습니다. 그렇다면 Miri가 우리가 필요한 avx512 명령어를 에뮬레이션할 수 있다면 정말 좋지 않을까요?
Miri는 이미 많은 avx2 명령어를 지원하지만, avx512에서는 qemu와 같은 문제에 부딪힙니다. 명령어 수가 너무 많고 전부 구현하는 일은 재미가 없습니다. 다행히 zlib-rs에 필요한 명령어는 몇 개뿐이며, Miri 코드베이스는 접근성이 꽤 좋아서 필요한 것들만 구현을 기여하는 것이 가능해 보였습니다.
결국 추가로 4개의 명령어 지원이 필요했습니다. 그리고 이들은 모두 Miri가 이미 지원하던 명령어의 “더 넓은(wider)” 버전이어서, 실제로는 더 좁은 구현과 코드를 공유합니다. 우리는 4개보다 더 많은 avx512 명령어를 사용하지만, 그중 다수는 메모리에서 값을 옮기는 동작에 관한 것이고 Miri는 이미 그런 작업을 처리할 줄 압니다. 유일하게 다른 흥미로운 명령어인 vpclmulqdq는 이미 구현되어 있었습니다.
추가한 인트린식은 다음과 같습니다(링크는 이를 구현한 PR/커밋을 가리킵니다):
_mm512_sad_epu8: 절대 차이의 합(sum of absolute differences). 이 연산은 두 개의 512비트 벡터를 인자로 받고, 입력을 두 개의 u8x8x8 행렬로 해석하여 대응하는 행들 사이의 절대 거리(absolute distance)를 계산한 다음 그 값을 합산합니다._mm512_ternarylogic_epi32: 이 연산은 3개의 벡터 인자를 받아 열(column) 단위로 순회하면서, 해당 열의 비트들을 8비트 마스크로 인덱싱하기 위한 인덱스로 사용합니다. 이를 통해 and, xor, 또는 열에서 2개 이상의 비트가 설정되어 있는지 확인하는 것 같은 다양한 논리 함수를 3개의 비트 벡터에 대해 구현할 수 있습니다._mm512_maddubs_epi16: 여러 곱셈과 덧셈을 하나의 명령어로 결합한 연산입니다._mm512_permutexvar_epi32: 런타임에 알려진 인덱스를 기반으로 SIMD 벡터의 요소들을 재배치하는(permutation) 연산입니다. 여기서는 좋은 테스트 케이스를 찾는 것이 조금 까다로웠습니다._mm512_ternarylogic_epi32의 경우 stdarch의 테스트만으로는 동작을 제대로 고정하기에 부족해서, 그쪽 테스트도 개선했습니다.
새 인트린식을 지원하는 과정을 보기 위해, 절대 차이의 합 PR을 조금 더 자세히 살펴보겠습니다. PR은 rust-lang/miri#4686입니다.
먼저 src/shims/x86/avx512.rs에 새 분기(branch)를 추가해 명령어의 LLVM 이름으로 매칭합니다. 인자가 2개인지 확인한 다음, 헬퍼 함수로 전달합니다. 이 헬퍼는 입력 폭(width)에 대해 제네릭이므로, avx2 구현도 같은 헬퍼를 호출하게 됩니다.
rust// Used to implement the _mm512_sad_epu8 function. "psad.bw.512" => { this.expect_target_feature_for_intrinsic(link_name, "avx512bw")?; let [left, right] = this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?; psadbw(this, left, right, dest)? }
제네릭 헬퍼는 대체로 다음과 같은 구조를 가집니다. 짧은 설명 주석과, 이 함수가 구현하는 인트린식에 대한 자세한 정보 링크가 있고, 정상 경로(happy path)에서는 interp_ok(/* ... */)를 반환합니다.
rust/// Compute the absolute differences of packed unsigned 8-bit integers /// in `left` and `right`, then horizontally sum each consecutive 8 /// differences to produce unsigned 16-bit integers, and pack /// these unsigned 16-bit integers in the low 16 bits of 64-bit elements /// in `dest`. /// /// <https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm_sad_epu8> /// <https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm256_sad_epu8> /// <https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm512_sad_epu8> fn psadbw<'tcx>( ecx: &mut crate::MiriInterpCx<'tcx>, left: &OpTy<'tcx>, right: &OpTy<'tcx>, dest: &MPlaceTy<'tcx>, ) -> InterpResult<'tcx, ()> { let (left, left_len) = ecx.project_to_simd(left)?; let (right, right_len) = ecx.project_to_simd(right)?; let (dest, dest_len) = ecx.project_to_simd(dest)?; // actual implementation interp_ok(()) }
그다음 이 경우 구현은 입력 폭을 검증하는 것부터 시작합니다.
rust// fn psadbw(a: u8x16, b: u8x16) -> u64x2; // fn psadbw(a: u8x32, b: u8x32) -> u64x4; // fn vpsadbw(a: u8x64, b: u8x64) -> u64x8; assert_eq!(left_len, right_len); assert_eq!(left_len, left.layout.layout.size().bytes()); assert_eq!(dest_len, left_len.strict_div(8));
마지막으로 Miri API를 사용한 실제 구현입니다.
rustfor i in 0..dest_len { let dest = ecx.project_index(&dest, i)?; let mut acc: u16 = 0; for j in 0..8 { let src_index = i.strict_mul(8).strict_add(j); let left = ecx.project_index(&left, src_index)?; let left = ecx.read_scalar(&left)?.to_u8()?; let right = ecx.project_index(&right, src_index)?; let right = ecx.read_scalar(&right)?.to_u8()?; acc = acc.strict_add(left.abs_diff(right).into()); } ecx.write_scalar(Scalar::from_u64(acc.into()), &dest)?; }
여기서 strict_{mul, add}와 명시적 변환을 사용한 점에 주목하세요. Miri는 오버플로우가 발생하는 산술을 크게(명확히) 실패하게 만들기를 원합니다.
구현이 어떻게 되어야 하는지 파악하려면, 해당 타깃 인트린식이 실제로 무엇을 하는지 제대로 이해하기 위해 약간의 실험이 필요합니다. 우리는 코너 케이스를 자극하는 테스트 케이스를 추가하고, 실제 하드웨어에서 그 테스트를 검증합니다.
우리의 요구에 맞게 컴파일러와 관련 도구를 개선할 수 있다는 점에는 꽤 강력한 ‘권한 부여’ 느낌이 있습니다. 이번 경우에는 인트린식 지원을 추가하는 것뿐 아니라, 벡터 폭에 걸쳐 구현을 일반화하는 작업도 자주 했고 Miri 테스트도 확장했습니다. 또한 stdarch에서도 자잘한 수정할 점을 몇 가지 찾았습니다. 전반적으로 아주 성공적인 작은 프로젝트였습니다.
avx512 구현은 최신 zlib-rs 및 libz-rs-sys 릴리스 0.5.3에서 사용할 수 있습니다. 실제로 avx512 알고리즘을 사용하려면 Rust 1.89 이상이 필요하며, 관련 타깃 기능을 활성화한 상태로 빌드해야 합니다. 예를 들어 -Ctarget-cpu=native 또는 -Ctarget-feature=+avx512vl,+avx512bw를 사용할 수 있습니다.
Miri 메인테이너 여러분, 특히 PR들에 대해 철저히 리뷰해 준 Ralf Jung에게 감사드립니다.
zlib-rs는 Trifecta Tech Foundation의 데이터 압축 이니셔티브의 일부입니다. zlib-rs를 재정적으로 지원하는 데 관심이 있으시다면 연락해 주세요.
Trifecta Tech Foundation
Castellastraat 26
6512 EX Nijmegen
The Netherlands
Follow us