POPLAR TUTORIAL 6: MATRIX-VECTOR MULTIPLICATION OPTIMISATION

Sungho Kim·2023년 11월 3일

IPU Programming

목록 보기
7/7

2.6. 포플러 튜토리얼 6: 행렬-벡터 곱셈 최적화

언제나 그렇듯, 이 튜토리얼을 보완하려면 주저하지 말고 Poplar 및 PopLibs 사용자 가이드를 읽어보세요 .

설정

IPU에서 이 튜토리얼을 실행하려면 Poplar SDK 환경을 활성화해야 합니다(IPU 시스템 시작 안내서 참조 ).

또한 C++11 표준과 호환되는 C++ 도구 체인이 필요합니다. 이 튜토리얼의 빌드 명령은 GCC를 사용합니다.

행렬-벡터 곱셈 최적화

이전 튜토리얼에서 우리는 행렬과 벡터를 곱하는 더 복잡한 정점을 만드는 방법을 배웠습니다. 그러나 IPU와 같은 대규모 병렬 시스템의 경우 튜토리얼 5의 전략이 가장 효율적이지 않습니다. 특히:

  • 각 행에 하나의 정점을 할당하면 시스템의 모든 작업자를 점유할 만큼 충분한 정점이 생성되지 않을 수 있습니다.

  • 입력 벡터는 모든 타일에 브로드캐스팅되어야 하므로 통신 비용이 많이 듭니다.

보다 효율적인 전략은 각 행을 여러 세그먼트로 분할하고 정점이 입력 벡터의 해당 세그먼트와 해당 행 세그먼트의 내적을 계산하도록 하는 것입니다. 이러한 부분합을 계산한 후 최종 출력 값을 얻기 위해 각 출력 요소에 대한 모든 부분합을 합산하는 축소 작업이 필요합니다.

이 튜토리얼에서는 최상의 성능을 얻기 위해 간단한 알고리즘을 사용하여 타일 전체에 데이터를 분할하는 가장 좋은 방법을 추정합니다. PopLibs 행렬 곱셈 함수는 사용하기에 가장 적합한 지침과 텐서 데이터를 재구성하는 다양한 방법을 고려하는 유사하지만 더 정교한 방법을 사용합니다.

이 튜토리얼에서는 완료해야 할 코드가 없습니다. 목표는 코드를 이해하고 다양한 행렬 크기를 실험하는 것입니다. 명령줄 옵션을 사용하여 --device코드가 실행되는 장치를 선택할 수 있습니다. 기본적으로 Mk2는 IPUModelIPU 하드웨어 동작의 시뮬레이션으로 사용됩니다.

의 장치 코드에는 벡터의 값 집합을 합하는 이라는 matrix-mul-codelets.cpp추가 정점 클래스가 포함되어 있습니다 .ReduceVertex

호스트 파일은 이전 튜토리얼과 동일한 구조를 따릅니다. 이 예의 차이점은 기능에 있습니다 buildMultiplyProgram. 이것이 수행하는 첫 번째 작업은 행렬 행을 분할할 세그먼트 수를 계산하는 것입니다.

// Get the optimal column axis split to split the number of columns
// into partial sums
unsigned colAxisSplit = calcOptimalColAxisSplit(graph, numRows, numCols);

함수 를 살펴보면 calcOptimalColAxisSplit가능한 모든 분할을 반복하고 estimateCycles 해당 분할에 대한 함수를 호출하는 것을 볼 수 있습니다. 함수 estimateCycles자체는 계산을 수행하는 데 걸리는 사이클 수를 추정하려고 시도합니다. 이는 부분합 계산 단계와 축소 단계 모두에 관련된 타일의 최악의 실행 시간과 교환 시간을 살펴봄으로써 수행됩니다. 에서 추정된 주기는 estimateCycles사용자가 수동으로 조정할 수 있습니다. 이 튜토리얼에서 정확한 숫자의 선택은 가정을 기반으로 합니다. 신뢰할 수 있는 사이클 수를 얻으려면 코드를 구현하고 하드웨어에서 실행하는 것이 중요합니다.

분할이 결정되면 코드는 중간 부분합 계산을 보관할 새 텐서를 생성합니다.

// Create a tensor to hold the intermediate calculated partial sums
auto partials = graph.addTensor("float", {numRows, colAxisSplit}, "partials");

계산은 두 단계로 나누어집니다. 첫 번째 단계에서는 모든 행 세그먼트의 내적을 계산하고 텐서에 씁니다 partials. 두 번째 단계에서는 partials텐서를 읽고 부분합을 더한 다음 출력을 최종 out텐서에 씁니다.

이 두 단계는 두 개의 루프로 구성됩니다. 첫 번째는 mulCS컴퓨팅 세트를 채웁니다.

// Create a compute set to hold the vertices to perform the
// partial sum calculations.
ComputeSet mulCS = graph.addComputeSet("mulCS");

// Create a vertex for each segment, for each row.
for (unsigned i = 0; i < colAxisSplit; ++i) {
    ...
    auto v = graph.addVertex(mulCS, "DotProductVertex",
    ...
}

두 번째 루프는 컴퓨팅 세트를 구축합니다 reduceCS.

// Create a compute set to calculate the reduction.
auto reduceCS = graph.createComputeSet("reduceCS");

// For each output element create a vertex.
for (unsigned row = 0; row < numRows; ++row) {
...
...
auto v = graph.addVertex(reduceCS, "ReduceVertex",
...
...

전체 곱셈을 수행하는 최종 프로그램은 두 개의 컴퓨팅 세트를 순서대로 실행하는 것으로 구성됩니다.

return Sequence({Execute(mulCS), Execute(reduceCS)});

마지막에 프로그램은 printProfileSummary메모리 사용에 대한 정보와 실행 및 통신 주기 수를 표시하는 함수를 호출합니다.

이 예제에는 makefile이 포함되어 있으므로 를 실행하여 빌드할 수 있습니다 make. 그런 다음 다양한 크기의 데이터에 대해 프로그램을 실행해 보세요. 예를 들어:

./tut6 10000 1000
Multiplying matrix of size 10000x1000 by vector of size 1000
Constructing compute graph and control program
Best split chosen:
colsAxisSplit=7, total cost=3996 (compute cost=3696,
                                  exchange cost=143,
                                  reduce exchange cost=49,
                                  reduce compute cost=108)
Worst cost seen: 53807
Running graph program to multiply matrix by vector
Multiplication result OK

이 출력 뒤에는 프로필 데이터가 옵니다.

위 출력에서 프로그램이 각 행을 7개의 세그먼트로 분할하고 예상 주기 비용이 3,996주기임을 알 수 있습니다.

프로필 출력에는 많은 정보가 포함됩니다. 우리와 가장 관련된 섹션은 "실행"이라는 제목 아래에 있습니다. 다음과 같은 내용이 표시됩니다.

Execution:

Programs executed:

<anonymous>.

  Total cycles:                                         6,681,766 (approx 5,023.9 microseconds)
  Tile average compute cycles (including idle threads): 3,801.8 (0.1% of total)
  Tile average compute cycles (excluding idle threads): 3,717.6 (0.1% of total)
  Tile average IPU exchange cycles:                     8,697.4 (0.1% of total)
  Tile average global exchange cycles:                  0.0 (0.0% of total)
  Tile average host exchange cycles:                    6,663,550.8 (99.7% of total)
  Tile average sync cycles:                             1,134.8 (0.0% of total)
우리가 가장 관심을 갖는 수치는 다음과 같습니다.

Tile average compute cycles (excluding idle threads): 3,717.6 (0.1% of total)

이는 모든 타일에 대한 평균 계산 주기 수 이며 프로그램 추정치인 3996에 매우 가깝습니다 IPUModel . 여기서 사용된 이후 프로파일링 시 제공된 숫자는 추정되며 하드웨어에서 실행될 때 실행 프로파일링과 다를 수 있습니다(이 IPUModel의 설명 참조). ).

"총 사이클" 줄은 프로그램을 실행하는 데 걸린 전체 시간입니다. 이를 단일 타일이 차지하는 주기 수로 생각할 수도 있습니다. 컴퓨팅, 교환, 동기화, 호스트 I/O의 총 주기입니다.

"타일 평균 호스트 교환 주기" 라인은 모든 타일에서 호스트와 데이터를 주고받는 데 사용되는 평균 주기 수를 알려줍니다. 이를 "총 주기" 숫자에서 빼면 타일 하나에 대한 계산 + 동기화 + 교환 주기를 얻게 됩니다.

PopVision 그래프 분석 도구를 사용하면 프로그램 동작에 대해 훨씬 더 자세한 통찰력을 얻을 수 있습니다. 프로그램은 profile.pop그래프 분석기가 읽을 수 있는 파일을 작성합니다. 그래프 분석기에 대한 자세한 내용은 PopVision 그래프 분석기 사용 설명서를 참조하십시오 .

메모:

  • Mk1 IPU 모델에서 이 튜토리얼을 실행하려면 명령이 다음과 같이 변경됩니다.
./tut6 10000 1000 --device model-ip1
  • 이 튜토리얼은 IPU 하드웨어로도 실행할 수 있습니다. 명령은 다음과 같이 변경됩니다.
./tut6 10000 1000 --device ipu

실행 프로필은 다음과 같습니다.

Execution:

Programs executed:

<anonymous>.

  Total cycles:                                         25,444,984 (approx 19,131.6 microseconds)
  Tile average compute cycles (including idle threads): 28,300.3 (0.1% of total)
  Tile average IPU exchange cycles:                     8,743.1 (0.0% of total)
  Tile average global exchange cycles:                  0.0 (0.0% of total)
  Tile average host exchange cycles:                    2,641,488.4 (10.4% of total)
  Tile average sync cycles:                             135,849.6 (0.5% of total)

IPU 하드웨어를 사용하는 타일당 총 주기는 IPU 모델을 사용할 때보다 훨씬 더 큽니다. 주요 오버헤드는 StreamCopyBegin프로그램에서 발생합니다. StreamCopyBegin 호스트가 I/O를 준비하는 동안 소요된 주기를 측정합니다 . 교환 패브릭의 대기 시간을 줄이기 위해 이 시뮬레이션 모델의 교환 구성은 단순하게 설정됩니다. 이전 사이클 추정치는 실제로 수작업으로 제작된 어셈블러에서만 볼 수 있는 이론적 최적 사이클 수를 가정했습니다. 단순화를 위해 이 튜토리얼에서는 사이클 수가 훨씬 더 높은 C++ 정점을 사용합니다.

profile
오복, 무심

0개의 댓글