数一数 float 孤岛 — 算子支持与替换
한국어 원문으로 표시합니다.
목표
같은 모델을 다섯 가지로 내보낸 뒤, 그래프에서 int8 로 감싸인 연산자와 float 로 남은 섬을 세는 분석기 graphscan.py 를 만든다. 범위를 좁히는 두 가지 옵션의 효과를 숫자로 확인하고, 모델을 Gemm 으로 고쳐 Q/DQ 왕복을 줄인다.
왜 중요한가
양자화 도구는 자기가 아는 연산자만 int8 모양으로 바꾼다. QDQ 형식에서는 연산자 이름이 그대로 남고 텐서마다 QuantizeLinear 와 DequantizeLinear 가 끼워질 뿐이다. 그래서 "양자화했다" 와 "int8 로 돈다" 는 다른 말이고, 둘을 가르려면 그래프를 세어야 한다. 이 실습은 판정 규칙을 이렇게 고정한다. 모든 입력이 DequantizeLinear 에서 오고 모든 출력이 QuantizeLinear 로만 가는 연산자를 int8 로 감싸인 것으로 본다. 나머지는 float 로 남고, 이어진 것끼리 묶으면 float 섬이 된다. 섬은 개수보다 경계가 문제다. 섬마다 들어오는 DQ 와 나가는 Q 가 붙고, 경계마다 값을 옮겨 담는 일과 반올림이 한 번씩 더 생긴다. 섬을 줄이는 길은 둘이다. 범위를 좁혀 어긋나는 노드를 아예 빼거나, 모델을 고쳐 양자화되는 모양으로 만들거나. 채점기는 여러분이 적은 숫자를 믿지 않는다. 매번 다른 층 수·모양·제외 노드로 자기 모델을 임시 디렉터리에 만들어 여러분의 분석기를 실제로 실행하고, 채점기가 같은 규칙으로 센 값과 대조한다. 6·7단계는 여러분이 만든 모델 파일을 채점기가 직접 읽어 다시 셉니다.
단계
- /root/ops/gen_models.py 를 만들어 실행해
/root/ops아래 모델 다섯 개를 만드세요. - /root/ops/graphscan.py 에
nodes를 만들어 연산자 census 와 초기값 자료형을 세게 하세요. qdq를 더해 QuantizeLinear·DequantizeLinear 와 Q/DQ 왕복을 세게 하세요.islands를 더해 int8 로 감싸인 연산자와 float 섬을 가르게 하세요.boundary를 더해 섬마다 드나드는 경계를 세게 하세요.diff를 더해 두 모델의 차이를 내게 하고, 범위를 좁힌 결과를 /root/ops/scope.json 에 적으세요.- MatMul 과 Add 를 Gemm 으로 합친 /root/ops/gemm.onnx 와 그것을 양자화한 /root/ops/gemm_full.onnx 를 만들고 /root/ops/fuse.json 에 적으세요.
- /root/ops/ops_report.md 를 네 절로 쓰세요.
참고
- 파이썬은
/opt/onnx-lab/bin/python입니다. 시스템python3에는 onnx 가 없습니다. - 실행 계약:
/opt/onnx-lab/bin/python /root/ops/graphscan.py <명령> <모델...>. 성공하면 종료 코드 0, 파일이 없으면 3, 사용법이 틀리면 2 입니다. 답은 JSON 한 덩어리로 표준출력에 냅니다. - 1단계가 만드는 다섯 파일:
fp32.onnx(원본),full.onnx(정적 QDQ, 범위를 안 좁힌 판),matmul_only.onnx(op_types_to_quantize=["MatMul"]),excluded.onnx(nodes_to_exclude로 MatMul 하나를 뺀 판),dynamic.onnx(quantize_dynamic). 원본은 MatMul·Add·Relu 로 층을 셋 이상 쌓고 노드마다 이름을 붙입니다. 입력 이름은x입니다. nodes응답:{"model", "nodes", "op_types", "initializers"}. op_types 는 연산자 이름을 키로 한 개수이고, initializers 는 초기값의 자료형 이름(onnx.TensorProto.DataType.Name이 주는 FLOAT·INT8 같은 이름)을 키로 한 개수입니다.qdq응답:{"model", "quantize", "dequantize", "round_trips", "weight_dequantize"}. round_trips 는 출력이 DequantizeLinear 로 곧장 들어가는 QuantizeLinear 의 수이고, weight_dequantize 는 첫 입력이 초기값인 DequantizeLinear 의 수입니다.islands응답:{"model", "compute", "wrapped", "float", "island_count", "islands"}. compute 는 QuantizeLinear·DequantizeLinear 를 뺀 연산자 수입니다. islands 의 각 항목은{"nodes", "size", "op_types"}이고 nodes 와 op_types 는 정렬합니다. 섬 목록은 첫 노드 이름 순으로 정렬합니다. 이름이 빈 노드는연산자이름_자리번호로 부릅니다.- 감싸임 판정: 노드의 모든 입력이 DequantizeLinear 의 출력이고, 모든 출력이 QuantizeLinear 에만 소비되면 감싸인 것입니다. 소비자가 하나도 없는 출력이 있으면 감싸이지 않은 것입니다.
boundary응답:{"model", "island_count", "crossings", "per_island"}. per_island 의 각 항목은{"nodes", "in_dequantize", "out_quantize"}이고, 같은 노드를 여러 번 세지 않습니다. crossings 는 전부 더한 값입니다.diff <a> <b>응답:{"a", "b", "op_delta", "quantize_delta", "dequantize_delta", "round_trip_delta", "island_delta", "float_delta"}. 각 delta 는 b 에서 a 를 뺀 값이고, op_delta 는 0이 아닌 항목만 담습니다.scope.json모양:{"excluded_node": 이름, "models": {"full": {...}, "matmul_only": {...}, "excluded": {...}}, "diff_matmul_only": {...}, "diff_excluded": {...}}. models 의 각 항목은{"quantize","dequantize","round_trips","island_count","float","crossings"}이고, diff 둘은 a 를 full.onnx 로 두고 낸diff응답 그대로입니다.fuse.json모양:{"before": {...}, "after": {...}, "removed_quantize": 정수, "removed_round_trips": 정수, "max_abs_error": 실수}. before 는 full.onnx, after 는 gemm_full.onnx 에 대한{"op_types","quantize","dequantize","round_trips"}입니다. max_abs_error 는 fp32.onnx 와 gemm.onnx 를 같은 입력으로 돌렸을 때의 최대 절대 오차입니다.- Gemm 으로 합치기:
MatMul의 출력이 그Add에서만 쓰이고Add의 다른 입력이 초기값일 때만 합칩니다.Gemm(A, B, C)는 기본값 alpha=1.0, beta=1.0, transA=0, transB=0 으로A @ B + C입니다. 합친 뒤onnx.checker.check_model을 지나야 하고, 원본과 같은 답을 내야 합니다. - 동적 양자화 판은 형식이 다릅니다.
MatMulInteger와DynamicQuantizeLinear가 나오고 QuantizeLinear·DequantizeLinear 는 없습니다. 섬과 경계를 재는 잣대는 QDQ 판을 위한 것이니, 동적 판에는nodes만 쓰는 것이 정직합니다. - 공식 문서: 양자화 · 그래프 최적화 · QuantizeLinear · DequantizeLinear · MatMulInteger · Gemm
- 흔한 실수: 파일 크기만 보고 양자화가 됐다고 하기, QDQ 판에서 MatMul 이 보인다고 양자화가 안 됐다고 하기, 같은 DequantizeLinear 를 여러 번 세기, 동적 판에 QDQ 잣대를 대기.
- 이 실습은 시간·처리량을 재지 않습니다. 세는 것만 다룹니다.
같은 모델을 다섯 가지로 내보내기
/root/ops/gen_models.py 를 만들어 실행해 /root/ops 아래 fp32.onnx·full.onnx·matmul_only.onnx·excluded.onnx·dynamic.onnx 를 만드세요.
onnx.helper 로 MatMul·Add·Relu 를 이어 붙이고 노드마다 이름을 주세요. 이름이 있어야 뒤에서 nodes_to_exclude 로 하나를 뺄 수 있습니다. 정적 양자화에는 CalibrationDataReader 가 필요하고, 같은 보정 자료를 여러 번 쓰려면 읽개를 그때마다 새로 만드세요 — 한 번 다 읽은 읽개는 다시 돌려주지 않습니다.
무엇이 들어 있는지부터 세기
/root/ops/graphscan.py 에 nodes <model> 을 만들어 연산자 census 와 초기값 자료형 census 를 내게 하세요.
초기값의 자료형은 onnx.TensorProto.DataType.Name(init.data_type) 으로 이름을 얻습니다. 정적 QDQ 판과 동적 판을 나란히 세어 보면 형식이 얼마나 다른지 바로 보입니다 — 동적 판에는 MatMul 이 아예 없습니다.
Q/DQ 왕복을 세기
qdq <model> 을 더해 quantize·dequantize·round_trips·weight_dequantize 를 세게 하세요.
가중치에 붙는 DequantizeLinear 는 짝이 되는 QuantizeLinear 가 없습니다. 가중치는 이미 int8 로 파일에 들어 있기 때문입니다. 그래서 DequantizeLinear 수에서 그만큼을 빼야 활성값의 왕복 수와 맞아떨어집니다.
float 로 남은 섬 가려내기
islands <model> 을 더해 int8 로 감싸인 연산자와 float 로 남은 연산자를 가르고, 이어진 것끼리 묶어 섬으로 내게 하세요.
먼저 생산자·소비자 표를 만들어 두면 나머지가 쉬워집니다. 감싸임 판정은 참고 절의 규칙을 그대로 쓰세요. 섬을 묶을 때는 float 노드 사이의 연결만 따라갑니다 — Q 나 DQ 를 지나면 다른 섬입니다.
섬마다 경계를 세기
boundary <model> 을 더해 섬마다 들어오는 DequantizeLinear 수와 나가는 QuantizeLinear 수를 세고 전체 경계 수를 내게 하세요.
같은 DequantizeLinear 가 한 섬의 두 노드에 들어갈 수 있습니다. 노드마다 세지 말고 집합으로 모아 세세요. 섬의 출력이 그래프 출력으로 곧장 나가면 나가는 QuantizeLinear 가 0일 수 있습니다.
범위를 좁히면 무엇이 달라지는가
diff <a> <b> 를 더해 두 모델의 차이를 내게 하고, 세 판의 수치와 두 diff 를 /root/ops/scope.json 에 참고 절의 모양으로 적으세요.
옵션을 주었다고 그대로 되었다고 믿지 마세요. 노드 이름으로 뺀 판에서는 그 노드의 가중치가 float 초기값으로 남아 있고, 연산자 종류로 좁힌 판에서는 그 종류 밖의 연산자가 통째로 섬이 됩니다. 두 좁히기는 그래프에 남기는 흔적이 다릅니다.
모델을 고쳐 왕복을 줄이기
MatMul 과 Add 를 Gemm 하나로 합친 /root/ops/gemm.onnx 를 만들고, 그것을 같은 설정으로 양자화한 /root/ops/gemm_full.onnx 를 만든 뒤, 두 판의 수치와 최대 절대 오차를 /root/ops/fuse.json 에 적으세요.
합치기 조건을 지키세요 — MatMul 의 출력이 그 Add 에서만 쓰이고, Add 의 다른 입력이 초기값일 때만 합칩니다. 합친 뒤에는 onnx.checker.check_model 을 지나야 하고, 원본과 같은 입력으로 돌려 답이 같은지 반드시 확인하세요. 값이 달라졌다면 합치기가 틀린 것입니다.
그래프로 답할 수 있게 적기
/root/ops/ops_report.md 를 ## 무엇이 int8 로 갔나 ## float 로 남은 섬 ## 범위를 좁히면 무엇이 달라지나 ## 모델을 고쳐 얻은 것 네 절로 쓰세요. 섬의 개수와 사라진 Q/DQ 수가 숫자로 들어가야 합니다.
이 보고서는 "왜 기대만큼 안 줄었나" 라는 질문에 답하는 글입니다. 세어 본 다섯 숫자(연산자 census·Q/DQ 왕복·섬 개수·섬 크기·경계 수)를 그대로 쓰면 답이 됩니다. 시간을 재지 않았다는 것도 적으세요.