MLIR 패스 경계에서 NaN 속성을 조기에 찾아 수정하고, 연산 검증기로 수치 계약을 강제하는 방법을 설명합니다.
다단계 중간 표현(MLIR) 패스는 상수 폴딩 중 숫자가 아님(NaN) 속성을 만들 수 있습니다. 모든 피연산자가 유효한 값으로 시작하더라도 이런 일이 발생할 수 있습니다.
컴파일러는 가장 이른 중간 표현(IR) 경계에서 그 값을 거부해야 합니다. 그렇지 않으면 런타임이 나중에 이를 멀리 떨어진 정확도 회귀로 드러냅니다.
가장 짧고 신뢰할 수 있는 절차는 첫 번째 문제 패스를 추적하고 그 산술 연산을 수정하는 것입니다. 그런 다음 로컬 연산 검증기가 연산의 수치 계약을 모든 패스 이후의 검사로 바꿉니다.
저는 신경망 처리 장치(NPU) 런타임에서 작업을 줄이는 퓨전 패스를 작성합니다. 이후의 회귀가 홀드아웃 평가 세트에서 더 낮은 정확도 지표를 보고하지만, 보고서는 어떤 퓨전 패스인지 식별하지 않습니다.
모델과 런타임을 거슬러 올라가며 문제 값을 추적합니다. 퓨전된 연산의 출력 속성에는 런타임이 이를 건드리기도 전에 이미 조용한 NaN이 들어 있습니다.
퓨전 패스가 0 * Inf를 그 속성으로 폴딩합니다. 런타임은 그 속성을 소비하고 남은 계산 전체에 NaN을 전파합니다.
연산은 여전히 타입 검사를 통과하고 피연산자와 결과도 계속 정렬되어 있습니다. 따라서 컴파일러는 구조적으로는 유효하지만 수치적으로는 의미 없는 IR을 생성합니다.
LLVM은 컴파일 타임 부동소수점 값을 APFloat로 표현합니다. 이 타입은 여러 부동소수점 형식과 명시적인 반올림 모드를 지원하므로, 컴파일러 코드는 호스트 머신의 네이티브 부동소수점 동작에 의존할 필요가 없습니다.
전기 전자 기술자 협회(IEEE) 754 표준은 0 * Inf를 NaN으로 정의합니다. 각 피연산자는 그 자체로 유효한 의미를 가질 수 있지만, 그 곱에는 수치 결과가 없습니다.
다른 산술 경로도 같은 종류의 값을 만들 수 있습니다.
Inf - Inf는 차이에 정의된 값이 없으므로 NaN을 생성합니다.Inf / Inf와 0 / 0은 어느 비율에도 정의된 값이 없으므로 NaN을 생성합니다.일반적인 산술 연산은 기존 NaN을 전파합니다. 따라서 한 번의 잘못된 폴딩이 런타임이 눈에 보이는 실패를 보고하기 전에 많은 다운스트림 연산으로 퍼질 수 있습니다.
NaN은 자기 자신과도 같지 않게 비교됩니다. NaN에 대한 모든 순서 비교는 거짓을 반환하므로, x < min || x > max 같은 범위 검사는 NaN을 마치 범위 안에 있는 것처럼 허용합니다.
직접적인 유한성 검사는 NaN과 무한대를 모두 잡습니다. 범위 검사는 이 계약을 대체할 수 없습니다.
하드웨어 기술 언어(HDL) 시뮬레이터는 알 수 없는 논리 상태를 나타내기 위해 X를 사용합니다. 초기화되지 않은 레지스터나 타이밍 위반은 하나의 X를 도입할 수 있고, 다운스트림 논리는 이를 소스에서 멀리 전파할 수 있습니다.
NaN도 같은 디버깅 형태를 따릅니다. 최종 관측은 전파의 증거를 제공하며, 유효한 값에서 NaN으로의 첫 전환이 결함을 식별합니다.
비유는 전파에서 끝납니다. X는 물리적인 세 번째 논리값이 아니라 시뮬레이터의 불확실성을 나타내는 반면, IEEE 754는 NaN을 지정된 비교 및 산술 동작을 가진 부동소수점 값으로 정의합니다.
이 차이는 디버깅 규칙을 바꾸지 않습니다. 첫 소스까지 거슬러 추적하십시오.
Synopsys는 레지스터 전송 수준(RTL) 및 게이트 수준 X 전파에 대해 같은 과정을 설명합니다. 엔지니어는 가장 이른 X가 발생할 때까지 드라이버와 팬인 신호를 따라갑니다.
조용한 NaN은 그래프가 런타임에 도달하기 전에 연산 속성에 들어 있습니다. 컴파일러는 퓨전 패스가 이를 만드는 정확한 경계에서 값을 검사할 수 있습니다.
런타임 수치 검사는 값이 컴파일러/런타임 경계를 지난 뒤에 증상을 관측합니다. 어느 컴파일러 패스가 속성을 처음 기록했는지는 식별할 수 없습니다.
MLIR 개발자 가이드는 모든 패스의 계약을 정의합니다. 각 패스는 유효한 입력 IR을 가정할 수 있고, 각 패스는 유효한 출력 IR을 반환해야 합니다.
패스 관리자는 기본적으로 패스 사이에서 이 계약을 강제합니다. 호출자는 패스별 검증을 비활성화할 수 있지만, 유효 입력 및 유효 출력 규약은 여전히 올바른 패스 동작을 정의합니다.
패스 경계 검증은 패스의 최종 출력을 검사합니다. 재작성은 내부적으로 일시적인 무효 상태를 사용할 수 있지만, 반환하기 전에 모든 불변 조건을 복원해야 합니다.
MLIR은 또한 연산 검증기에 로컬 속성을 검사하라고 지시합니다. 검증기는 생산자나 소비자를 따라가지 않고도 연산 자체 속성의 값을 검사할 수 있습니다.
이 로컬 규칙은 변환의 자유를 보존합니다. 또한 거부 범위를 연산 자체가 정의하는 불변 조건으로 제한합니다.
MLIR 패스 파이프라인은 고정된 순서로 실행됩니다. 출력에 NaN이 들어 있는 첫 번째 패스가 소스 경계를 표시합니다.
초기 분류 과정에서 -mlir-print-ir-after-all은 모든 패스 뒤의 IR을 출력합니다. 출력이 의심스러운 패스를 드러내면, 대상 플래그가 중요한 두 상태를 보여 줍니다.
-mlir-print-ir-before=<pass>
-mlir-print-ir-after=<pass>
-mlir-print-ir-after-change 플래그는 IR을 변경하지 않는 패스의 출력을 억제합니다. 이 옵션은 많은 패스가 주어진 입력에 영향을 주지 않는 파이프라인에서 잡음을 줄입니다.
비공개 연산, 속성, 패스 이름은 비공개로 유지합니다. 다음의 가상 양자화 재조정 연산은 관련 구조를 보존합니다.
// 퓨전 패스 이전의 마지막 정상 IR.
%0 = "quant.rescale_fuse"(%input) {scale = 2.500000e-01 : f32}
: (tensor<1x64x56x56xf32>) -> tensor<1x64x56x56xf32>
// 퓨전 패스가 0 * inf를 폴딩한 뒤의 첫 번째 문제 IR.
%0 = "quant.rescale_fuse"(%input) {scale = 0x7FC00000 : f32}
: (tensor<1x64x56x56xf32>) -> tensor<1x64x56x56xf32>
피연산자, 결과 타입, 형상은 동일하게 유지됩니다. scale 속성만 유한한 1/4 배율 인자에서 조용한 NaN의 비트 패턴으로 바뀝니다.
타입 검사기나 형상 추론 패스는 두 형태 모두를 허용합니다. 연산에는 두 번째 형태를 거부할 수치 불변 조건이 필요합니다.
0 * Inf 폴딩을 수정하면 현재 소스는 제거됩니다. 하지만 다른 패스, 임포터 또는 재작성 패턴이 같은 속성에 NaN을 할당하는 일까지 막지는 못합니다.
연산 검증기는 지속적인 계약을 명시합니다. 가상의 quant.rescale_fuse 연산에서 scale은 유한한 양의 실수 범위에 속하는 곱셈 양자화 인자를 나타냅니다.
그 의미는 유한성과 양수성을 연산에 내재적인 조건으로 만듭니다. 모든 생산자는 같은 제약을 지켜야 합니다.
연산이 일반적으로 NaN을 허용하지만 한 퓨전 패스만 이를 만들면 안 되는 경우에는, 패스가 검사를 소유해야 합니다. 연산 검증기는 연산 의미론이 허용하는 값을 거부해서는 안 됩니다.
연산 정의 명세(ODS)는 선언 하나로 사용자 지정 검증기를 활성화합니다.
def RescaleFuseOp : Quant_Op<"rescale_fuse", []> {
let arguments = (ins AnyTensor:$input, F32Attr:$scale);
let results = (outs AnyTensor:$output);
let hasVerifier = 1;
}
C++ 구현은 완전한 속성 계약을 검사합니다.
LogicalResult RescaleFuseOp::verify() {
const llvm::APFloat &scale = getScaleAttr().getValue();
if (!scale.isFinite() || scale.isNegative() || scale.isZero())
return emitOpError() << "scale attribute must be a finite, positive value";
return success();
}
APFloat::isNaN()은 조용한 NaN 및 신호 NaN 인코딩을 거부하지만, 양수 및 음수 무한대는 허용합니다. APFloat::isFinite()은 NaN과 두 무한대를 모두 거부합니다.
어떤 질의가 적합한지는 연산의 의미론이 결정합니다. 양자화 배율은 무한대를 사용할 수 없으므로, 이 검증기는 유한성과 양수성을 검사합니다.
ODS 문서는 검증 순서를 정의합니다. 구조적 트레이트가 먼저 실행되고, 생성된 불변 조건 검사가 다음으로 속성과 타입을 검증하며, 사용자 지정 검증기는 그 검사 뒤에 실행됩니다.
이 순서 덕분에 verify()는 getScaleAttr().getValue()를 직접 호출할 수 있습니다. 생성된 검사가 이미 속성의 존재와 타입을 확립했습니다.
MLIR 테스트 가이드는 연산 불변 조건을 위한 -verify-diagnostics 테스트를 문서화합니다. 하나의 부정 사례가 검증기가 NaN을 거부하고 진단을 보존함을 증명합니다.
// RUN: mlir-opt %s -split-input-file -verify-diagnostics
func.func @rescale_rejects_nan(%arg0: tensor<4xf32>) -> tensor<4xf32> {
// expected-error@+1 {{scale attribute must be a finite, positive value}}
%0 = "quant.rescale_fuse"(%arg0) {scale = 0x7FC00000 : f32}
: (tensor<4xf32>) -> tensor<4xf32>
return %0 : tensor<4xf32>
}
주변 연산 테스트는 적어도 하나의 유효한 배율 사례를 유지해야 합니다. 함께 사용하면 이 사례들은 허용과 거부 동작을 모두 보존합니다.
마지막 정상/첫 번째 문제 비교는 문제가 되는 패스를 찾습니다. 하지만 재현 사례에서 관련 없는 구조를 제거하지는 않습니다.
mlir-reduce는 사용자가 정의한 흥미도 테스트를 보존하면서 유효한 입력을 최소화합니다. 각 후보에 축소를 적용하기 전에 검증합니다.
이 도구는 테스트가 실패를 재현하는 동안에만 축소를 유지합니다.
이 사고에서 후보는 퓨전 패스 이전의 유효한 IR을 포함해야 합니다. 흥미도 테스트는 의심스러운 파이프라인을 실행하며, 검증기가 scale attribute must be a finite, positive value를 보고할 때만 성공합니다.
이미 무효인 퓨전 이후 연산을 mlir-reduce에 제공하지 마십시오. 이 입력은 도구가 축소하기 전에 검증에 실패합니다.
대신 도구에 유효한 퓨전 이전 IR을 제공하십시오. 그러면 패스가 무효한 속성을 만들게 하는 조건을 축소할 수 있습니다.
구체적인 명령은 비공개 다이얼렉트와 파이프라인에 따라 달라집니다.
# 테스트 스크립트와 함께 mlir-reduce 실행
mlir-reduce first-good-input.mlir \
-reduction-tree="traversal-mode=0 test=check_for_nan_diagnostic.sh"
패스 관리자 검증을 활성화한 상태로 실패를 재현합니다.
-mlir-print-ir-after-all로 첫 번째 문제 패스를 찾습니다.
대상 출력 플래그로 해당 패스 전후의 IR을 캡처합니다.
무효한 값을 만드는 산술 연산을 수정합니다.
불변 조건이 연산에 속할 때 로컬 연산 검증기를 추가합니다.
하나의 부정 진단 회귀 테스트를 추가합니다.
같은 실패 판정식으로 유효한 패스 이전 입력을 축소합니다.
무효한 값이 실행 데이터에만 존재할 때는 런타임 수치 검사가 여전히 유용합니다. 입력 의존적 불안정성은 컴파일 타임 속성에 절대 나타나지 않을 수 있습니다.
이 사고에는 다른 경계가 있습니다. 컴파일러가 이미 무효한 값을 보유하고 있으므로, 연산 검증기는 런타임이 멀리 떨어진 정확도 회귀를 보고하게 두는 대신 깨진 계약 바로 옆에서 컴파일을 중단합니다.