백엔드 크래시 보고하기
컴파일이 실패한 뒤 대량의 llvm-ir 코드가 출력된다면 enzyme 백엔드가 코드를 컴파일하지 못했을 가능성이 높습니다. 이러한 경우는 디버깅하기가 더 어려우므로 여러분의 도움에 크게 감사드립니다. 또한 현재로서는 대개 릴리스 빌드가 동작할 가능성이 훨씬 높다는 점도 염두에 두십시오.
여기서 최종 목표는 enzyme 컴파일러 익스플로러에서 버그를 재현하여 Enzyme 저장소에 버그 보고를 작성하는 것입니다.
이를 돕기 위해 rustflags에 전달할 수 있는 autodiff 플래그가 있습니다. 이 플래그는 전체 llvm-ir 모듈을 몇 개의 __enzyme_fwddiff 또는 __enzyme_autodiff 호출과 함께 출력합니다. Linux에서 가능한 워크플로는 다음과 같습니다:
llvm-ir 생성 제어하기
llvm-ir을 생성하기 전에, 관련 rust 코드가 디버깅할 때 확실히 보이도록 하는 데 도움이 되는 두 가지 기법을 염두에 두십시오.
std::hint::black_box: rust 변수나 표현식을std::hint::black_box()로 감싸 rust와 llvm이 이를 최적화하여 제거하지 못하게 하십시오. 이는 llvm-ir에서 특정 값을 검사하거나 수동으로 조작해야 할 때 유용합니다.extern "rust"또는extern "c": 특정 함수 선언이 llvm-ir로 어떻게 낮춰지는지 보고 싶다면 해당 함수를extern "rust"또는extern "c"로 선언할 수 있습니다. 생성된 모듈 안에서 기존의__enzyme_autodiff또는 유사한 선언을 찾아 예시로 참고할 수도 있습니다.
1) llvm-ir 재현 파일 생성하기
RUSTFLAGS="-Z autodiff=Enable,PrintModBefore" cargo +enzyme build --release &> out.ll
이 방법은 모듈 위아래에 있는 몇 가지 경고와 정보 메시지도 함께 캡처합니다. out.ll을 열고 ; moduleid = <somehash> 위에 있는 모든 줄을 제거하십시오. 이제 파일의 끝부분을 보고 llvm-ir에 속하지 않는 모든 내용, 즉 오류와 경고를 제거하십시오. 이제 llvm-ir의 마지막 줄은 !<somenumber> = 로 시작해야 합니다. 예를 들면 !40831 = !{i32 0, i32 1037508, i32 1037538, i32 1037559} 또는 !43760 = !dilocation(line: 297, column: 5, scope: !43746)와 같습니다.
실제 숫자는 코드에 따라 달라집니다.
2) llvm-ir 재현 파일 확인하기
이전 단계가 제대로 되었는지 확인하기 위해 llvm의 opt 도구를 사용하겠습니다. <some_dir>/rust/build/<x86/arm/...-target-triple>/ci-llvm/bin/opt와 비슷한 opt 바이너리 경로를 찾으십시오. LLVM을 소스에서 빌드한다면 ci-llvm을 build로 바꿔야 할 가능성이 높습니다. 또한 /rust/build/target-triple/enzyme/build/enzyme/llvmenzyme-21과 비슷한 llvmenzyme-21.<so/dll/dylib> 경로도 찾으십시오. llvm은 llvm 백엔드를 자주 업데이트하므로 버전 번호가 더 높을 수 있다는 점(20, 21, …)을 염두에 두십시오. 둘 다 찾았다면 다음 명령을 실행하십시오.
<path/to/opt> out.ll -load-pass-plugin=/path/to/build/<target-triple>/stage1/lib/libEnzyme-21.so -passes="enzyme" -enzyme-strict-aliasing=0 -S
이 명령은 향후 버전이나 여러분의 시스템에서 실패할 수 있으며, 그런 경우에는 libEnzyme-21.so를 LLVMEnzyme-21.so로 바꿔야 합니다. 빌드 방법에 대한 안내는 Enzyme 문서를 참고하십시오. LLVM 버전을 빌드하는 방법도 조정해야 할 수 있습니다.
이전 단계가 성공했다면 cargo로 rust 코드를 컴파일할 때 보았던 것과 같은 오류가 표시됩니다.
같은 오류가 나타나지 않는다면 rust 저장소에 이슈를 열어 주십시오. 성공했다면 축하합니다! 파일이 여전히 매우 크므로 자동으로 최소화해 봅시다.
3) llvm-ir 재현 코드 최소화하기
먼저 llvm-extract 바이너리를 찾으십시오. opt 바이너리와 같은 폴더에 있을 것입니다. 그런 다음 다음을 실행하십시오:
<path/to/llvm-extract> -S --func=<name> --recursive --rfunc="enzyme_autodiff*" --rfunc="enzyme_fwddiff*" --rfunc=<fnc_called_by_enzyme> out.ll -o mwe.ll
이 명령은 최소 작동 예제인 mwe.ll을 생성합니다.
마지막 --func 플래그로 전달하는 이름을 알맞게 바꾸십시오. 미분하려는 함수에 #[no_mangle] 속성을 적용한 뒤 해당 이름을 Rust 이름으로 바꿀 수 있습니다. 그렇지 않으면 맹글링된 함수 이름을 찾아야 합니다. 이를 위해 out.ll을 열고 __enzyme_fwddiff 또는 __enzyme_autodiff를 검색하십시오. 해당 함수 호출의 첫 번째 문자열이 여러분의 함수 이름입니다. 예시:
define double @enzyme_opt_helper_0(ptr %0, i64 %1, double %2) {
%4 = call double (...) @__enzyme_fwddiff(ptr @_zn2ad3_f217h3b3b1800bd39fde3e, metadata !"enzyme_const", ptr %0, metadata !"enzyme_const", i64 %1, metadata !"enzyme_dup", double %2, double %2)
ret double %4
}
여기서는 _zn2ad3_f217h3b3b1800bd39fde3e가 올바른 이름입니다. 앞에 붙은 @는 복사하지 않도록 주의하십시오. opt 명령을 다시 실행하여 2)단계를 반복하되, 이번에는 out.ll 대신 mwe.ll을 입력 파일로 전달하십시오. 이 최소화된 예제가 여전히 크래시를 재현하는지 확인하십시오.
4) (선택 사항) llvm-ir 재현 코드를 더 최소화하기
이전 단계를 마치면 약 5천 줄 분량의 mwe.ll 파일이 생겼을 것입니다. 이를 50줄까지 줄여 봅시다. opt 및 llvm-extract와 같은 위치에 있는 llvm-reduce 바이너리를 찾으십시오. 오류 메시지의 첫 줄을 복사하십시오. 예를 들면 다음과 같습니다:
opt: /home/manuel/prog/rust/src/llvm-project/llvm/lib/ir/instructions.cpp:686: void llvm::callinst::init(llvm::functiontype*, llvm::value*, llvm::arrayref<llvm::value*>, llvm::arrayref<llvm::operandbundledeft<llvm::value*> >, const llvm::twine&): assertion `(args.size() == fty->getnumparams() || (fty->isvararg() && args.size() > fty->getnumparams())) && "calling a function with bad signature!"' failed.
segfault만 발생한다면 의미 있는 오류 메시지가 없고 자동으로 할 수 있는 일도 많지 않으므로 5)단계로 넘어가십시오.
그렇지 않다면 다음 내용을 담은 script.sh 파일을 만드십시오
#!/bin/bash
<path/to/your/opt> $1 -load-pass-plugin=/path/to/llvmenzyme-19.so -passes="enzyme" \
|& grep "/some/path.cpp:686: void llvm::callinst::init"
grep에 어떤 오류 메시지를 전달할지 여러 가지로 시도해 보십시오. 오류를 고유하게 식별할 수 있을 만큼 충분히 길어야 합니다. 다만 ( 또는 )를 포함하는 긴 오류라면 이를 올바르게 이스케이프해야 하므로 번거로울 수 있습니다. 다음을 실행하십시오:
<path/to/llvm-reduce> --test=script.sh mwe.ll
input isn't interesting! verify interesting-ness test가 표시된다면 script.sh의 오류 메시지를 잘못 작성한 것이므로, grep이 실제 오류와 일치하는지 확인해야 합니다. 모든 것이 잘 진행되면 많은 반복이 수행된 후 새로운 reduced.ll 파일이 생성됩니다. opt로 여전히 같은 오류가 발생하는지 확인하십시오.
고급 디버깅: llvm-ir 수동 조사
최소화된 재현 코드(mwe.ll 또는 reduced.ll)를 얻었다면 더 깊이 파고들 수 있습니다:
- 수동 편집: llvm-ir을 직접 다시 작성해 보십시오. 간접 호출과 관련된 문제처럼 특정한 문제라면
__enzyme_virtualreverse와 같은 Enzyme 전용 인트린식을 조사해 볼 수 있습니다. 이를 사용하는 방법을 이해하려면 Enzyme의 문서나 소스 코드를 참고해야 할 수도 있습니다. - enzyme 테스트 케이스: 문제와 관련된 기능이나 인트린식의 올바른 사용법을 보여 줄 수 있는 테스트 케이스를 enzyme 저장소에서 찾아보십시오.
5) 버그를 보고하십시오.
그런 다음 mwe.ll(또는 reduced.ll) 예제를 복사하여 저희 컴파일러 탐색기에 붙여 넣을 수 있습니다.
- 언어로
llvm ir를, 컴파일러로opt 20을 선택하십시오. - 컴파일러 오른쪽 필드가 아직 설정되어 있지 않다면 그 값을
-passes="enzyme"로 바꾸십시오. - 이제는 익숙해진 오류가 다시 한번 나타날 것입니다.
- 공유 버튼을 사용하여 해당 링크를 복사하십시오.
- https://github.com/enzymead/enzyme/issues에 이슈를 생성하고
mwe.ll과 (있다면)reduced.ll, 그리고 컴파일러 탐색기 링크를 공유하십시오. Rust 코드나 그 코드의 링크도 자유롭게 추가하십시오.
조사 결과 문서화하기
"attempting to call an indirect active function whose runtime value is inactive"와 같은 일부 enzyme 오류는 그동안 혼란을 일으켜 왔습니다. 이러한 문제를 조사한다면, 완전한 해결책을 찾지 못하더라도 조사 결과를 문서화하는 것을 고려해 주십시오. 얻은 통찰이 Rust에서의 사용에 한정되지 않고 enzyme 전반에 해당한다면, 이를 메인 enzyme 문서에 기여하는 것이 대개 가장 좋은 첫걸음입니다. 관련 enzyme GitHub 이슈에 조사 결과를 언급하거나, 적절하다면 이 문서의 수정을 제안할 수도 있습니다. 이렇게 하면 다른 사람들이 처음부터 다시 시작하지 않아도 됩니다.
명확한 재현 예제와 문서가 있다면 enzyme 개발자가 버그를 고칠 수 있을 것입니다. 버그가 고쳐지면 Rust 컴파일러 내부의 enzyme 서브모듈이 업데이트되며, 이를 통해 Rust 코드를 미분할 수 있게 될 것입니다. rust-ad 개선에 도움을 주셔서 감사합니다.
Rust 코드 최소화하기
최소한의 llvm-ir 재현 예제뿐만 아니라, 의존성이 없는 최소한의 Rust 재현 예제가 있는 것도 도움이 됩니다. 이렇게 하면 버그를 고친 후 이를 CI 테스트 케이스로 추가할 수 있어 향후 회귀를 방지할 수 있습니다.
Rust 재현 예제를 최소화하는 데 도움이 되는 몇 가지 방법이 있습니다. 아마 가장 간단한 자동화 방법은 cargo-minimize입니다.
그 밖에도 treereduce, halfempty, picireny, 그리고 경우에 따라 creduce 등 다양한 대안이 있습니다.