2016-08-19 7 views
6

長さのある2つのベクトルの間に1D畳み込みを実行するコードをMetalで実装しようとしています。私は正しく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; 
    } 
} 

私の問題は、それが(GPUからの/計算+データ転送)を処理するのに約10倍の時間を要している作品次のCPU上でスウィフトよりも金属を使用して同じデータを実装しました。私の質問は、私は内部ループを単一のベクトル演算に置き換えるか、上記のコードを高速化する別の方法はありますか?

+0

あなたのカーネル関数は完全にシリアル形式で書かれており、GPUの並列性を利用していません。しかし、それを最適化する前に、データベクトルの大きさと頻度はどのくらい変わるのでしょうか?データを転送する時間が処理時間を支配する場合、GPUを使用するのは適切な方法ではないかもしれません。 – warrenm

+0

はい、@warrenmが既に指摘したように、あなたはGPUの並列性を利用していません。これは、GPUが効率的に処理する方法ではありません。 GPUにデータを送信する必要があります。これにより、各フラグメントが個別の乗算範囲を計算します。 – codetiger

+0

GPUの例はこちらですhttp://stackoverflow.com/questions/12576976/1d-convolution-without-if-else-statements-non-fft – codetiger

答えて

9

この場合、GPUの並列性を利用するための鍵は、外部ループを管理することです。データベクトル全体に対して一度カーネルを呼び出すのではなく、データベクトルの各要素に対してカーネルを呼び出すことになります。カーネル関数は、このように簡略化:この作品を派遣するために

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; 
} 

、我々は計算パイプライン状態が推奨するスレッド実行幅に基づいて、スレッドグループサイズを選択します。ここで難しいのは、入力バッファと出力バッファに十分なパディングがあることを確認して、データの実際のサイズを少しオーバーランさせることです。これは、私たちにメモリと計算量を浪費させますが、バッファの終わりにある要素のたたみ込みを計算するために、別のディスパッチを行う複雑さを軽減します。私の実験では

// 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() 

、この並列化されたアプローチは、問題のシリアルバージョンより400-1000x速く実行されます。私はそれがあなたのCPUの実装とどのように比較されているのか興味があります。

+0

あなたのコードは私のCPUバージョンより約450倍速く走ります。これは私の予想以上に速いです。このような例外的な答えをありがとう。 – Epsilon

+0

これはあなたのためにうまくいきました。 – warrenm

+0

こんにちは@warrenm。私はあなたが記述したものを実装しようとしていますが、 'makeBuffer(bytesNoCopy:length:options:deallocator:)'で 'MTLBuffer'を作成することはできません、_pointer 0x16fcbbd48というエラーメッセージが表示されます。私は長さが4096の倍数である配列を試しましたが、私はまだこのエラーを受け取ります... 'makeBuffer(bytes:length:options:)'は動作するようですが、ここでは出力バッファを取得する方法は分かりませんデータをfloat配列に戻します。 @Epsilonもしあなたの2人のうちのどちらかが私にボイラープレートコードを投稿したり、私に送ることができたら、私は非常に感謝しています。 –