节点从六个减到两个 — 数清优化改动了什么
한국어 원문으로 표시합니다.
목표
최적화가 손댈 거리를 모은 /root/onnxq-optimize/graph.onnx 를 직접 짓고, ORT 의 네 최적화 단계를 돌려 그래프를 파일로 꺼내 노드를 세는 도구 /root/onnxq-optimize/optlevel.py 를 만든다. 단계마다 무엇이 사라지고 무엇이 생기는지 적고, 답이 같은지 재고, 꺼낸 파일을 다른 기계로 옮기면 안 되는 이유를 도메인 이름으로 증명한다.
왜 중요한가
onnxruntime 은 세션을 열 때 그래프를 다시 쓴다. 상수만으로 계산되는 부분을 미리 접고, 없어도 되는 노드를 지우고, 자주 나오는 모양을 하나로 묶는다. 무엇이 어디까지 바뀌는지는 최적화 단계가 정한다.
양자화 전후 속도를 견줄 때 숫자가 안 맞는 원인이 여기 있는 경우가 많다. 두 측정의 최적화 단계가 다르면 그 비교는 양자화를 잰 것이 아니라 최적화를 잰 것이다. 기준선을 ORT_DISABLE_ALL 로 못 박아 두면 이 흔들림이 사라진다.
더 비싼 사고는 최적화된 파일을 저장해 배포하는 것이다. 그 파일에는 표준이 아닌 도메인의 노드가 들어 있어 다른 런타임이 열지 못한다. 그런데 onnx.checker 는 그 파일을 통과시킨다 — 검사기는 모르는 도메인을 지나가기 때문이다. 그래서 이식 가능 여부의 근거는 검사기가 아니라 노드의 도메인 목록이다.
이 실습은 시간을 재지 않는다. 이 기계는 에뮬레이션이라 같은 일도 두 배까지 흔들린다. 그래서 구조가 어떻게 바뀌었는지와 답이 같은지만 판정한다.
채점기는 여러분이 적어 낸 문구를 믿지 않는다. 임시 디렉터리에 채점기가 직접 지은 그래프를 차려 놓고 여러분의 도구를 실제로 실행해, 같은 파일을 채점기가 네 단계로 최적화해 얻은 값과 대조한다. 모양·가중치·난수 씨앗은 실행마다 바뀝니다.
단계
- /root/onnxq-optimize/build_graph.py 를 만들어 실행해 /root/onnxq-optimize/graph.onnx 를 만드세요.
- /root/onnxq-optimize/optlevel.py 에
nodes를 만들어 지금 그래프의 노드와 개수를 읽어 내게 하세요. optimize를 더해 주어진 단계로 최적화한 그래프를 파일로 꺼내게 하세요.levels를 더해 네 단계를 모두 돌려 노드를 세어 나란히 놓게 하세요.fusion을 더해 단계마다 무엇이 사라지고 무엇이 생겼는지 적게 하세요.equal을 더해 주어진 씨앗으로 만든 같은 입력을 네 단계에 넣어 답이 같은지 재게 하세요.portable을 더해 꺼낸 파일이 표준 연산자만 쓰는지 판정하게 하세요.- /root/onnxq-optimize/opt_report.json 과 /root/onnxq-optimize/opt_report.md 로 보고서를 만드세요.
참고
- 파이썬은 /opt/onnx-lab/bin/python 입니다. 시스템
python3에는 onnx 도 numpy 도 없습니다. 실행 예:/opt/onnx-lab/bin/python /root/onnxq-optimize/optlevel.py levels /root/onnxq-optimize/graph.onnx - 이 파드는 네트워크가 없습니다. 재료는 직접 짓습니다.
- 그래프 계약: 입력
x는 FLOAT 이고 축은 [심볼, 6], 출력y는 [심볼, 5] 입니다. 노드에는 MatMul·Add·Relu·Identity·Mul 이 들어가야 하고, 입력이 모두 initializer 인 노드가 하나 이상 있어야 합니다(그것이 접힐 상수입니다). 최적화를 전부 켰을 때 노드 수가 줄어야 합니다. - 실행 계약:
optlevel.py <명령> .... 답은 JSON 한 덩어리로 표준출력에 냅니다. 성공하면 종료 코드 0, 모르는 명령이면 2 입니다. - 단계 이름은
ORT_DISABLE_ALL·ORT_ENABLE_BASIC·ORT_ENABLE_EXTENDED·ORT_ENABLE_ALL넷입니다. nodes <모델>응답:{"nodes": [{"op_type", "domain", "name"}...], "count": {op_type: 개수}, "initializers": [이름 정렬]}. nodes 의 순서는 파일에 적힌 순서 그대로입니다.optimize <모델> <단계> <출력>은SessionOptions.graph_optimization_level을 그 단계로 두고optimized_model_filepath를 출력 경로로 두어 세션을 연 뒤, 저장된 파일을 다시 읽어{"level", "out", "nodes": [op_type...], "count", "initializers"}를 냅니다.levels <모델>응답: 단계 이름을 키로{"nodes": [op_type...], "count": {...}}를 담은 객체 넷입니다.fusion <모델>응답: 단계마다{"removed": 원본에 있었는데 사라진 op_type 정렬, "added": 새로 생긴 op_type 정렬, "total": 노드 수}입니다.equal <모델> <씨앗> <행수>는numpy.random.RandomState(씨앗).standard_normal((행수, 입력너비))를 float32 로 만들어 네 단계에 모두 넣습니다. 응답:{"seed", "rows", "levels": {단계: {"sum", "max", "max_abs_diff"}}, "max_abs_diff"}. sum 과 max 는 출력 전체의 합과 절댓값 최대이고, max_abs_diff 는 ORT_DISABLE_ALL 의 출력과의 최대 절대 차이입니다. 소수점 아래 여섯 자리까지 적으면 충분합니다.portable <모델>은ORT_ENABLE_ALL로 꺼낸 파일을 읽어{"nondefault_domains": 노드 도메인 가운데 표준이 아닌 것 정렬, "opset_domains": opset_import 의 도메인 정렬, "checker": "ok"|"error", "portable": 불리언}을 냅니다. 표준 도메인은 빈 문자열과ai.onnx입니다.opt_report.json에는levels(네 단계의nodes)·nondefault_domains·portable·equality(seed·rows·max_abs_diff)를 적습니다.opt_report.md는## 무엇이 줄었나## 어느 단계에서 무엇이 바뀌나## 답은 같은가## 이 파일을 옮겨도 되나네 절로 쓰고, 최적화 전 노드 수와 남은 도메인 이름을 본문에 적습니다.- ORT 는
ORT_ENABLE_ALL로 꺼낼 때 같은 환경에서만 쓰라는 경고를 표준오류에 냅니다.SessionOptions.log_severity_level을 2 로 두면 그 경고를 볼 수 있고, 3 으로 두면 조용해집니다. 경고는 표준오류로 가니 표준출력의 JSON 은 깨지지 않습니다. - 부동소수점: 단계가 달라지면 커널과 계산 순서가 달라집니다. float32 는 유효자리가 7자리라 마지막 자리가 흔들릴 수 있으니, 같은지 볼 때는 상대오차 1e-5 정도의 허용 오차를 두고 그 근거를 적어 두세요.
- 시간은 재지 않습니다. 이 기계는 에뮬레이션이라 같은 일도 두 배까지 흔들립니다.
- 공식 문서: Graph optimizations · ORT Python API · Gemm · Execution Providers
- 흔한 실수: 최적화된 파일을 그대로 배포하기, 검사기 통과를 이식 가능의 근거로 쓰기, 최적화 단계가 다른 두 측정을 견주기, 노드 수를 곧바로 속도로 읽기.
최적화가 손댈 거리 모으기
/root/onnxq-optimize/build_graph.py 를 만들어 실행해 /root/onnxq-optimize/graph.onnx 를 만드세요. 입력이 모두 initializer 인 노드 하나, Identity 하나, 그리고 MatMul·Add·Relu·Mul 이 들어가야 합니다.
상수 접기를 보려면 두 initializer 를 더하는 노드를 넣고 그 결과를 뒤에서 쓰면 됩니다. Identity 는 아무 일도 하지 않으니 없어도 되는 노드의 예로 딱 맞습니다. MatMul 다음에 Add 를 두고 그다음에 Relu 를 두면 융합되는 모양이 됩니다. 저장하기 전에 onnx.checker.check_model(model, full_check=True) 로 걸러 보세요.
지금 그래프를 세어 두기
/root/onnxq-optimize/optlevel.py 에 nodes <모델> 을 만들어 {"nodes", "count", "initializers"} 를 내게 하세요. nodes 의 각 항목에는 op_type·domain·name 을 담습니다.
노드의 domain 은 표준 연산자면 빈 문자열입니다. 지금은 전부 빈 문자열이겠지만, 최적화된 파일을 열면 그렇지 않은 것이 나옵니다. 그때 견주려면 지금 것을 먼저 세어 두어야 합니다. 순서는 파일에 적힌 그대로 두세요.
최적화된 그래프를 파일로 꺼내기
optimize <모델> <단계> <출력> 을 더해 그 단계로 최적화한 그래프를 출력 경로에 저장하고, 저장된 파일을 다시 읽어 노드를 세어 내게 하세요.
SessionOptions.optimized_model_filepath 에 경로를 주면 세션을 여는 동안 다시 쓴 그래프가 그 경로에 저장됩니다. 세션을 열지 않으면 파일도 안 생깁니다. ORT_DISABLE_ALL 로 꺼낸 것이 기준선이고, 원본과 같은 노드가 나와야 정상입니다.
네 단계를 나란히 놓기
levels <모델> 을 더해 네 단계를 모두 돌려 단계마다 {"nodes", "count"} 를 담은 객체를 내게 하세요.
임시 디렉터리에 단계마다 하나씩 꺼내 놓고 읽으면 됩니다. 끝나면 지우세요. 네 줄을 나란히 놓으면 어느 단계에서 무엇이 일어나는지가 한눈에 보입니다 — 문서를 읽는 것보다 이쪽이 빠릅니다.
무엇이 사라지고 무엇이 생겼나
fusion <모델> 을 더해 단계마다 {"removed", "added", "total"} 을 내게 하세요. removed 는 원본에 있었는데 사라진 op_type, added 는 새로 생긴 op_type 입니다.
원본의 op_type 집합과 각 단계의 op_type 집합을 견주면 됩니다. 상수만으로 계산되던 노드는 사라지고 그 결과가 initializer 로 들어앉습니다. MatMul 다음의 Add 는 Gemm 하나로 묶이고, 다음 단계에서는 활성까지 삼킨 이름이 나옵니다. 그 이름의 도메인이 무엇인지도 눈여겨보세요.
그래프는 바뀌어도 답은 같은가
equal <모델> <씨앗> <행수> 를 더해 그 씨앗으로 만든 같은 입력을 네 단계에 모두 넣고 {"seed", "rows", "levels", "max_abs_diff"} 를 내게 하세요.
입력은 numpy.random.RandomState(씨앗).standard_normal((행수, 입력너비)) 를 float32 로 만듭니다. 입력 너비는 모델 선언의 1번 축에서 읽으세요. 단계마다 출력 전체의 합과 절댓값 최대를 적고, 기준선과의 최대 절대 차이도 적습니다. 정확히 같기를 기대하지 말고 허용 오차를 정해 두세요 — float32 의 유효자리는 7자리뿐입니다.
이 파일을 옮겨도 되나
portable <모델> 을 더해 ORT_ENABLE_ALL 로 꺼낸 파일을 읽고 {"nondefault_domains", "opset_domains", "checker", "portable"} 을 내게 하세요.
표준 도메인은 빈 문자열과 ai.onnx 뿐입니다. 그 밖의 도메인을 가진 노드가 하나라도 있으면 그 파일은 그 환경 전용입니다. onnx.checker 는 그런 파일도 통과시키니 검사기 결과와 도메인 목록을 함께 적어 두세요 — 둘이 엇갈린다는 것이 이 단계의 핵심입니다. log_severity_level 을 2 로 두면 ORT 가 저장하면서 내는 경고도 볼 수 있습니다.
네 단계를 한 장으로 보고하기
/root/onnxq-optimize/opt_report.json 에 levels·nondefault_domains·portable·equality 를 적고, /root/onnxq-optimize/opt_report.md 를 ## 무엇이 줄었나 ## 어느 단계에서 무엇이 바뀌나 ## 답은 같은가 ## 이 파일을 옮겨도 되나 네 절로 쓰세요.
채점기가 같은 그래프를 직접 네 단계로 최적화해 노드를 세고, equality 에 적힌 씨앗과 행수로 다시 돌려 차이를 잽니다. 그러니 실제로 재서 적으세요. 보고서에는 최적화 전 노드 수와 남은 도메인 이름을 숫자와 이름 그대로 적어야 받는 쪽이 판단할 수 있습니다.