LabHub
배우기 러닝패스 코스

The AI Diet Gone Wrong

Six Nodes Down to Two: Counting What Optimization Changed

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

최적화가 손댈 거리를 모은 /root/onnxq-optimize/graph.onnx 를 직접 짓고, ORT 의 네 최적화 단계를 돌려 그래프를 파일로 꺼내 노드를 세는 도구 /root/onnxq-optimize/optlevel.py 를 만든다. 단계마다 무엇이 사라지고 무엇이 생기는지 적고, 답이 같은지 재고, 꺼낸 파일을 다른 기계로 옮기면 안 되는 이유를 도메인 이름으로 증명한다.

왜 중요한가

onnxruntime 은 세션을 열 때 그래프를 다시 쓴다. 상수만으로 계산되는 부분을 미리 접고, 없어도 되는 노드를 지우고, 자주 나오는 모양을 하나로 묶는다. 무엇이 어디까지 바뀌는지는 최적화 단계가 정한다. 양자화 전후 속도를 견줄 때 숫자가 안 맞는 원인이 여기 있는 경우가 많다. 두 측정의 최적화 단계가 다르면 그 비교는 양자화를 잰 것이 아니라 최적화를 잰 것이다. 기준선을 ORT_DISABLE_ALL 로 못 박아 두면 이 흔들림이 사라진다. 더 비싼 사고는 최적화된 파일을 저장해 배포하는 것이다. 그 파일에는 표준이 아닌 도메인의 노드가 들어 있어 다른 런타임이 열지 못한다. 그런데 onnx.checker 는 그 파일을 통과시킨다 — 검사기는 모르는 도메인을 지나가기 때문이다. 그래서 이식 가능 여부의 근거는 검사기가 아니라 노드의 도메인 목록이다. 이 실습은 시간을 재지 않는다. 이 기계는 에뮬레이션이라 같은 일도 두 배까지 흔들린다. 그래서 구조가 어떻게 바뀌었는지와 답이 같은지만 판정한다. 채점기는 여러분이 적어 낸 문구를 믿지 않는다. 임시 디렉터리에 채점기가 직접 지은 그래프를 차려 놓고 여러분의 도구를 실제로 실행해, 같은 파일을 채점기가 네 단계로 최적화해 얻은 값과 대조한다. 모양·가중치·난수 씨앗은 실행마다 바뀝니다.

단계

  1. /root/onnxq-optimize/build_graph.py 를 만들어 실행해 /root/onnxq-optimize/graph.onnx 를 만드세요.
  2. /root/onnxq-optimize/optlevel.pynodes 를 만들어 지금 그래프의 노드와 개수를 읽어 내게 하세요.
  3. optimize 를 더해 주어진 단계로 최적화한 그래프를 파일로 꺼내게 하세요.
  4. levels 를 더해 네 단계를 모두 돌려 노드를 세어 나란히 놓게 하세요.
  5. fusion 을 더해 단계마다 무엇이 사라지고 무엇이 생겼는지 적게 하세요.
  6. equal 을 더해 주어진 씨앗으로 만든 같은 입력을 네 단계에 넣어 답이 같은지 재게 하세요.
  7. portable 을 더해 꺼낸 파일이 표준 연산자만 쓰는지 판정하게 하세요.
  8. /root/onnxq-optimize/opt_report.json/root/onnxq-optimize/opt_report.md 로 보고서를 만드세요.

참고

최적화가 손댈 거리 모으기

/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.pynodes <모델> 을 만들어 {"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.jsonlevels·nondefault_domains·portable·equality 를 적고, /root/onnxq-optimize/opt_report.md## 무엇이 줄었나 ## 어느 단계에서 무엇이 바뀌나 ## 답은 같은가 ## 이 파일을 옮겨도 되나 네 절로 쓰세요.

채점기가 같은 그래프를 직접 네 단계로 최적화해 노드를 세고, equality 에 적힌 씨앗과 행수로 다시 돌려 차이를 잽니다. 그러니 실제로 재서 적으세요. 보고서에는 최적화 전 노드 수와 남은 도메인 이름을 숫자와 이름 그대로 적어야 받는 쪽이 판단할 수 있습니다.