CUDA のユニファイド メモリについて

ユニファイド メモリとは

CUDA 6 では、NVIDIA は、CUDA の歴史の中で最も重要なプログラミング モデルの改善の 1 つであるユニファイド メモリ (以降、UM と呼びます) を導入しました。今日の一般的な PC では、CPU のメモリと GPU は物理的に独立しており、PCI-E バスを介して接続および通信されます。実際、CUDA 6.0 より前は、プログラマーはプログラミング中にこれを認識し、コードに反映する必要がありました。メモリ割り当てはCPUとGPUの両端で行う必要があり、手動コピーは常に行われますが、

void sortfile(FILE *fp, int N)                       void sortfile(FILE *fp, int N)                   
{
    
                                                        {
    
    
    char *data;                                          char *data; 
    data = (char*)malloc(N);                             cudaMallocManaged(data, N);

    fread(data, 1, N, fp);                               fread(data, 1, N, fp);

    qsort(data, N, 1, compare);                          qsort<<<...>>>(data, N, 1, compare);
                                                         cudaDeviceSynchronize();

    usedata(data);                                       usedata(data);
    free(data);                                          free(data);
}

2 つのコードが驚くほど似ていることがはっきりとわかります。

唯一の違いは次のとおりです。

GPU バージョン:
1. malloc の代わりに cudaMallocManaged を使用してメモリを割り当てます。
2. CPU と GPU は非同期で実行されるため、カーネルの起動後に同期のために cudaDeviceSynchronize を呼び出す必要があります。
3. CUDA 6.0 より前の場合、上記の機能を実現するには、次のコードが必要になる場合があります。

void sortfile(FILE *fp, int N)    
{
    
    
    char *h_data, *d_data;                                        
    h_data= (char*)malloc(N); 
    cudaMalloc(&d_data, N);

    fread(h_data, 1, N, fp);  

    cudaMemcpy(d_data, h_data, N, cudaMemcpyHostToDevice);

    qsort<<<...>>>(data, N, 1, compare);

    cudaMemcpy(h_data, h_data, N, cudaMemcpyDeviceToHost);  //不需要手动进行同步,该函数内部会在传输数据前进行同步
    
    usedata(data);
    free(data); 
}

これまでのところ、主な利点は次のとおりです。

1. コード記述とメモリモデルを簡素化
2. CPU 側と GPU 側で別々にスペースを割り当てることなく、ポインタを共有できます。管理に便利で、コード量を削減します。
3. 言語がより緊密に統合され、互換性のある言語との文法上の違いが減少します。
4. より便利なコード移行。

ディープコピー

. . 先ほどの説明から判断すると、コード量はあまり削減されていないようです。. 次に、非常に一般的な状況を考えてみましょう。次のような構造体がある場合:

struct dataElem {
    
    
    int data1;
    int data2;
    char *text;
}

次のようにします。

void launch(dataElem *elem) 
{
    
    
    dataElem *d_elem;
    char *d_text;

    int textlen = strlen(elem->text);

    // 在GPU端为g_elem分配空间
    cudaMalloc(&d_elem, sizeof(dataElem));
    cudaMalloc(&d_text, textlen);
    // 将数据拷贝到CPU端
    cudaMemcpy(d_elem, elem, sizeof(dataElem));
    cudaMemcpy(d_text, elem->text, textlen);
    // 根据gpu端分配的新text空间,更新gpu端的text指针
    cudaMemcpy(&(d_elem->text), &d_text, sizeof(g_text));

    // 最终CPU和GPU端拥有不同的elem的拷贝
    kernel<<< ... >>>(g_elem);
}

しかし、CUDA 6.0 以降では、UM の参照により、次のようになります。

void launch(dataElem *elem) 
{
    
      
    kernel<<< ... >>>(elem); 
} 

明らかに、ディープ コピーの場合、UM によってコード量が大幅に削減されます。UM が登場する前は、両端のアドレス空間が同期していなかったため、メモリを手動で割り当ててコピーする作業を何度も行う必要がありました。特に非 CUDA プログラマーにとっては、非常に不快で扱いにくいものです。実際のデータ構造がより複雑になると、両者のギャップはより大きくなります。

共通データ構造連結リストに関しては、本質的にはポインタで構成されるネストされたデータ構造であり、UM を使用しない場合、CPU と GPU 間の共有連結リストは非常に扱いにくく、メモリ空間の転送が非常に複雑になります。

この時点で UM を使用すると、次の利点があります。

1. リンクされたリスト要素を CPU と GPU 間で直接転送します。
2. CPU または GPU のいずれかの端にあるリンク リスト要素を変更します。
3. 複雑な同期の問題を回避します。

しかし実際には、UM が登場する前は、ゼロコピー メモリ (固定ホスト メモリ) を使用してこの複雑な問題を解決できました。それでも、固定されたホストメモリのデータ取得は PCI-express のパフォーマンスに左右され、UM を使用することでより良いパフォーマンスが得られるため、UM の存在は依然として意味があります。この問題については、このホワイト ペーパーでは詳しく説明しません。

C++ でユニファイド メモリを使用する方法

最新の C++ では、malloc などのメモリ割り当て関数を明示的に呼び出すことを回避しようとするため、ラップには new が使用されます。したがって、UM は override new 関数を通じて使用できます。

class Managed {
    
    
    void *operator new(size_t len) 
   {
    
    
        void *ptr;
         cudaMallocManaged(&ptr, len);
         return ptr;
   }
    void operator delete(void *ptr) 
   {
    
    
        cudaFree(ptr);
   }
};

このクラスを継承することで、カスタム C++ クラスは UM の参照渡しを実現できます。コンストラクターで cudaMallocManaged を呼び出して、UM での値渡しを実現します。以下は、説明するための文字列クラスです。

// 通过继承来实现 pass-by-reference
class String : public Managed {
    
    
    int length;
    char *data;
    // 通过copy constructor实现pass-by-value
    String (const String &s) {
    
    
        length = s.length;
        cudaMallocManaged(&data, length);
        memcpy(data, s.data, length);
    }
};

ユニファイド メモリとユニファイド バーチャル アドレッシングの長所と短所の比較

実際、CUDA 4.0 は Unified Virtual Addressing (以下、UVA と呼びます) のサポートを開始しました。Unified Memory と混同しないでください。UM は UVA に依存していますが、実際には同じものではありません。この問題を明確にするために、まず UVA が気にするメモリの種類を知る必要があります。

1. デバイス メモリ (おそらく別の GPU 上)
2. オンチップ共有メモリ
3. ホスト メモリ

SM 内のローカルメモリやレジスタなどのスレッド関連のメモリについては、明らかに UVA の対象外です。したがって、UVA は実際にはこれらのメモリに統一されたアドレス空間を提供します.このため、UVA はゼロコピー テクノロジを有効にし、CPU 側でメモリを割り当て、CUDA VA をそれにマッピングし、PCI-E を介して各操作を実行します。また、UVA がメモリ移行を行うことは決してないことに注意してください。

この 2 つの間のより詳細な比較とパフォーマンス分析は、この記事の範囲を超えており、フォローアップ記事で説明する場合があります。

疑い:

1. Q: UM は、システム メモリと GPU メモリの間のコピーを排除しますか?
回答: いいえ、コピー作業のこの部分が実行時に実行するために CUDA に渡されるだけで、これはプログラマーに対してのみ透過的です。メモリ コピーのオーバーヘッドは依然として存在し、GPU 側と CPU 側のデータの一貫性を確保するために、競合状態の問題を考慮する必要があります。簡単に言えば、メモリを手動で管理する優れた能力を持っている場合、UM によってパフォーマンスが向上することはなく、作業負荷が軽減されるだけです。
2. 質問: データ間のコピーが解消されていないため、これはコンパイラの時間の問題のようですが、なぜ 3.0 以上を計算する必要があるのでしょうか? みんなをだましてカードを買わせるためですか?
待ってください...実際には、これまで多くの実装の詳細を省略してきました。原則としてコピーをなくすことはできないため、コンパイル時にすべてのメッセージを取得することはできません。そして重要なことに、Pascal 以降の GPU アーキテクチャでは、49 ビットの仮想メモリ アドレッシングとオンデマンド ページ マイグレーション機能が提供されます。49 ビットのアドレッシング長は、GPU がシステム メモリ全体とすべての GPU メモリをカバーするのに十分です。ページ移行エンジンは、GPU スレッドが非常駐メモリにアクセスできるように、任意のアドレス可能な範囲のメモリをメモリを介して GPU メモリに移行します。
要するに、新しいアーキテクチャ カードは、GPU がプログラム コードを変更することなく「余分な」メモリにアクセスできるようにするため、GPU はアウトオブコア操作 (つまり、処理するデータがローカル メモリを超える操作) を処理できます。物理メモリ)。
さらに、Pascal と Volta は、複数の GPU にまたがるシステム全体のアトミック メモリ操作もサポートしており、マルチ GPU の下では、コードの複雑さを大幅に簡素化できます。
同時に、データが分散しているプログラムの場合、オンデマンド ページ マイグレーション機能により、メモリ全体をロードする代わりに、ページ フォールトを介してより細かい粒度でメモリをロードできるため、データ マイグレーションのコストをさらに節約できます。(実はCPUも昔から似たようなもので、原理はよく似ています。)

おすすめ

転載: blog.csdn.net/daijingxin/article/details/122462167