2016-08-19 36 views
6

Próbuję zaimplementować kod w metalu, który wykonuje splot 1D między dwoma wektorami o długościach. I zostały wdrożone następujące która działa poprawnieJak przyspieszyć Metal Code dla iOS/Mac OS

kernel void convolve(const device float *dataVector [[ buffer(0) ]], 
        const device int& dataSize [[ buffer(1) ]], 
        const device float *filterVector [[ buffer(2) ]], 
        const device int& filterSize [[ buffer(3) ]], 
        device float *outVector [[ buffer(4) ]], 
        uint id [[ thread_position_in_grid ]]) { 
    int outputSize = dataSize - filterSize + 1; 
    for (int i=0;i<outputSize;i++) { 
     float sum = 0.0; 
     for (int j=0;j<filterSize;j++) { 
      sum += dataVector[i+j] * filterVector[j]; 
     } 
     outVector[i] = sum; 
    } 
} 

Moim problemem jest to, że trwa około 10 razy dłużej do przetworzenia (transfer danych obliczeniowych + do/z GPU) tych samych danych przy użyciu metalu niż w Swift na CPU. Moje pytanie brzmi: jak zastąpić wewnętrzną pętlę pojedynczą operacją wektorową lub czy istnieje inny sposób na przyspieszenie powyższego kodu?

+0

Twoja funkcja kernel jest napisany w sposób całkowicie seryjny, i nie skorzystać z równoległości GPU. Jednak zanim przystąpisz do optymalizacji, jak duży jest twój wektor danych i jak często się zmienia? Jeśli czas przeniesienia danych dominuje nad czasem przetwarzania, użycie GPU może nie być właściwym rozwiązaniem. – warrenm

+0

Tak, jak już podkreślił @warrenm, nie korzystasz z równoległości w GPU. Nie jest tak, ponieważ procesory graficzne działają wydajnie. Musisz wysłać dane do GPU, aby każdy fragment obliczał oddzielny zakres mnożenia. – codetiger

+0

Przykład GPU jest tutaj http://stackoverflow.com/questions/12576976/1d-convolution-without-if-else-statements-non-fft – codetiger

Odpowiedz

9

Kluczem do wykorzystania paralelności GPU w tym przypadku jest umożliwienie zarządzania pętlą zewnętrzną. Zamiast raz wywoływać jądro dla całego wektora danych, wywołajmy go dla każdego elementu w wektorze danych. Funkcja jądra upraszcza to:

kernel void convolve(const device float *dataVector [[ buffer(0) ]], 
        const constant int &dataSize [[ buffer(1) ]], 
        const constant float *filterVector [[ buffer(2) ]], 
        const constant int &filterSize [[ buffer(3) ]], 
        device float *outVector [[ buffer(4) ]], 
        uint id [[ thread_position_in_grid ]]) 
{ 
    float sum = 0.0; 
    for (int i = 0; i < filterSize; ++i) { 
     sum += dataVector[id + i] * filterVector[i]; 
    } 
    outVector[id] = sum; 
} 

W celu wysłaniu tej pracy, możemy wybrać rozmiar threadgroup oparciu o szerokości wykonania gwint zalecanej przez państwo rurociągu obliczeniowej. Jedną z najtrudniejszych rzeczy jest upewnienie się, że w buforze wejściowym i wyjściowym jest wystarczająca ilość podkładek, abyśmy mogli nieco przekroczyć rzeczywisty rozmiar danych. To powoduje, że marnujemy niewielką ilość pamięci i obliczeń, ale oszczędzamy nam złożoności wykonywania oddzielnej wysyłki tylko po to, aby obliczyć splot dla elementów na końcu bufora.

// We should ensure here that the data buffer and output buffer each have a size that is a multiple of 
// the compute pipeline's threadExecutionWidth, by padding the amount we allocate for each of them. 
// After execution, we ignore the extraneous elements in the output buffer beyond the first (dataCount - filterCount + 1). 

let iterationCount = dataCount - filterCount + 1 
let threadsPerThreadgroup = MTLSize(width: min(iterationCount, computePipeline.threadExecutionWidth), height: 1, depth: 1) 
let threadgroups = (iterationCount + threadsPerThreadgroup.width - 1)/threadsPerThreadgroup.width 
let threadgroupsPerGrid = MTLSize(width: threadgroups, height: 1, depth: 1) 

let commandEncoder = commandBuffer.computeCommandEncoder() 
commandEncoder.setComputePipelineState(computePipeline) 
commandEncoder.setBuffer(dataBuffer, offset: 0, at: 0) 
commandEncoder.setBytes(&dataCount, length: MemoryLayout<Int>.stride, at: 1) 
commandEncoder.setBuffer(filterBuffer, offset: 0, at: 2) 
commandEncoder.setBytes(&filterCount, length: MemoryLayout<Int>.stride, at: 3) 
commandEncoder.setBuffer(outBuffer, offset: 0, at: 4) 
commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) 
commandEncoder.endEncoding() 

W moich doświadczeniach, to parallelized podejście prowadzi 400-1000x szybciej niż w wersji seryjnej w pytaniu. Jestem ciekawy, jak to się ma do twojej implementacji procesora.

+0

Twój kod działa około 450 razy szybciej niż moja wersja procesora. To jest znacznie szybsze, niż się spodziewałem. Dziękuję za tak wyjątkową odpowiedź. – Epsilon

+0

Zadowolony z tego, że to zadziałało tak dobrze. – warrenm

+0

Witam @ warrenm. Staram się realizować to, co opisane, ale nie mogę utworzyć 'MTLBuffer' z' makeBuffer (bytesNoCopy: Długość: opcje: dealokator:) ', otrzymuję komunikat o błędzie _pointer 0x16fcbbd48 nie jest 4096 bajtów aligned_. Próbowałem z tablicą, w której długość jest wielokrotnością 4096 i nadal otrzymuję ten błąd ... 'makeBuffer (bytes: length: options:)' wydaje się działać, ale tutaj nie wiem jak uzyskać bufor wyjściowy dane z powrotem do tablicy float. @Epsilon, jeśli którykolwiek z was dwóch może wysłać lub wysłać mi cały kod płyty kotła, byłbym niezmiernie wdzięczny ... –