CUDA統一內存UVA

介紹
設備是否支持統一內存可以通過一下代碼查詢:

//如果支持,unifiedAddressing字段爲1
int deviceCount;
cudaGetDeviceCount(&deviceCount);
int device;
for (device = 0; device < deviceCount; ++device) {
    cudaDeviceProp deviceProp;
    cudaGetDeviceProperties(&deviceProp, device);
    printf("Device %d has compute capability %d.%d.\n",
           device, deviceProp.major, deviceProp.minor);
}

當應用程序是64位進程,並且主機和所有具有計算能力2.0(附錄說的是3.0以上)及更高版本的設備都使用一個地址空間,此時通過CUDA API分配的所有主機內存和所有受支持設備上開闢的設備內存都在此虛擬地址範圍內。
o 通過CUDA接口分配的主機內存或者任何使用統一地址空間的設備分配的設備內存都可以使用cudaPointerGetAttributes()的指針來確定地址。
o 使用統一內存進行數據拷貝不需要執行拷貝類型,只需使用cudaMemcpyDefault,這同樣適用於不使用CUDA API分配的主機內存,只要設備使用的是統一地址。
o 如果使用統一地址空間,通過cudaHostAlloc()分配的頁鎖定主機內存會默認cudaHostAllocPortable,此時指針可以直接在內核函數使用,而無需像頁鎖定內存那樣先通過cudaHostGetDevicePointer()函數獲取設備指針。

void *ptr;
   //分配頁鎖定內存默認cudaHostAllocPortable,指針可以直接在覈函數使用
cudaHostAlloc(&ptr, 1000, cudaHostAllocDefault);
   //分配統一內存
   cudaMallocManaged(&ptr, 1000);
   //host_ptr爲malloc開闢的內存
   memcpy(ptr, host_ptr, 1000);
//也可直接使用cudaMemcpy
   cudaMemcpy(ptr, host_ptr, 1000, cudaMemcpyDefault);

統一內存是CUDA編程模型的一個組成部分,CUDA 6.0首次介紹了該模型,它定義了一個託管(managed)內存空間,其中所有處理器(包括CPU和GPU)都可以看到共同的地址空間。
通過讓底層系統自己管理CUDA數據訪問和位置,避免顯式數據拷貝,這主要帶來兩方面好處:
o 簡化編程
o 通過透明地將數據遷移到使用它的處理器,可以最大限度地提高數據訪問速度。
簡單來說就是統一內存消除了顯式數據拷貝並且不會像zero-copy那樣帶來性能下降(頁鎖定內存分配過多性能會下降),當然數據遷移仍然會發生,所以程序速度不會明顯加快,不過可以簡化代碼編寫和維護。
統一內存提供“單一指針”模型,這有點像zero-copy。主要不同點是零拷貝分配的內存是固定主機內存,程序的性能可能快也可能慢,這取決於從哪裏訪問。而統一內存分離內存和執行空間,所以數據訪問很快。
統一內存是一套內存管理服務的系統,該系統的一部分定義了加入統一內存服務的託管內存(managed memory)空間,換句話說,managed memory只是統一內存的一部分。
統一內存可以像其他設備內存一樣使用CUDA的任何操作,最主要的區別就是主機可以直接引用和訪問統一內存。
統一內存不支持附加在Tegra上的離散GPU。
系統要求
o SM架構是3.0(Kepler)或者更高
o 64位程序並且是非嵌入式系統
如果SM的架構是6.x(Pascal)或者更高,統一內存有新功能,如按需頁面遷移和GPU內存超量分配(oversubscription),請注意,目前只有Linux操作系統支持這些功能。在Windows(無論是在TCC或WDDM模式下)或MacOS上運行的應用程序將使用基本的統一內存模型,就像在6.x之前的體系結構上一樣,即使它們運行在具有6.x或更高計算能力的硬件上。
簡化編程

__global__ void AplusB(int *ret, int a, int b) {
    ret[threadIdx.x] = a + b + threadIdx.x;
}
int main() {
    int *ret;
    //第一種分配方式
    cudaMallocManaged(&ret, 1000 * sizeof(int));
    AplusB<<< 1, 1000 >>>(ret, 10, 100);
    cudaDeviceSynchronize();
    for(int i = 0; i < 1000; i++)
        printf("%d: A+B = %d\n", i, ret[i]);
    cudaFree(ret); 
    return 0;
}
//第二種分配方式
__device__ __managed__ int ret[1000];
__global__ void AplusB(int a, int b) {
    ret[threadIdx.x] = a + b + threadIdx.x;
}
int main() {
    AplusB<<< 1, 1000 >>>(10, 100);
    cudaDeviceSynchronize();
    for(int i = 0; i < 1000; i++)
        printf("%d: A+B = %d\n", i, ret[i]);
    return 0;
}

以上代碼未使用cudaMemcpy(),所以未使用到隱式同步,所以需要做顯式同步cudaDeviceSynchronize()。
數據遷移和一致性
統一內存試圖通過將數據遷移到正在訪問它的設備來優化內存性能(即,如果CPU正在訪問數據,則將數據移動到主機內存;如果GPU將訪問數據,則將數據移動到設備內存)。數據遷移是統一內存的基礎,但對程序是透明的。系統將嘗試將數據放置在可以最有效地訪問數據的位置,而不會違反一致性。
對於程序來說,數據的物理地址是不可見的,隨時都可能變化,但是訪問數據的虛擬地址是保持有效和一致。注意,在性能之前,保持一致性是主要要求;在主機操作系統的限制範圍內,允許系統失敗訪問或移動數據,以保持處理器之間的全局一致性。
計算能力低於6.x的GPU體系結構不支持按需將託管數據細粒度移動到GPU。每當啓動GPU內核時,通常必須將所有託管內存(managed memory)傳輸到GPU內存,以避免內存訪問出錯。隨着計算能力6.x到來,引入了一種新的GPU頁面錯誤機制,提供更無縫的統一內存功能。結合系統範圍的虛擬地址空間,頁面錯誤提供了幾個好處。首先,頁面錯誤意味着CUDA系統軟件不需要在每個內核啓動之前將所有託管內存分配同步到GPU。如果在GPU上運行的內核訪問一個不在其內存中的頁面,那麼它會出錯,允許該頁面按需自動遷移到GPU內存。或者,可以將頁面映射到GPU地址空間,以便通過PCIe或NVLink互連進行訪問(訪問時的映射有時比遷移更快)。注意,統一內存是系統範圍的:GPU(和CPU)可以在內存頁上發生故障並從CPU內存或系統中其他GPU的內存遷移內存頁。
顯存超量分配
計算能力低於6.x的設備無法分配比顯存的物理大小更多的託管內存。具有計算能力6.x的設備擴展了尋址模式以支持49位虛擬尋址。它的大小足以覆蓋現代CPU的48位虛擬地址空間,以及GPU顯存。巨大的虛擬地址空間和頁面錯誤功能使應用程序能夠訪問整個系統虛擬內存,而不受任何一個處理器的物理內存大小的限制。這意味着應用程序可以超額訂閱內存系統:換句話說,它們可以分配、訪問和共享大於系統總物理容量的數組,從而實現超大型數據集的核心外處理。只要有足夠的系統內存可供分配,cudaMallocManaged就不會耗盡內存。
多GPU設備
對於計算能力低於6.x的設備來說,managed memory分配的行爲和其他非managed內存一樣,內存分配實際物理地址是當前活動設備,其他GPU設備都接收相同的內存映射。這意味着其他GPU將通過PCIe總線以較低的帶寬訪問內存。如果系統中的GPU之間不支持對等映射,那麼託管內存頁將放置在CPU系統內存(“零拷貝”內存)中,並且所有GPU都將遇到PCIe帶寬限制。
具有計算能力6.x設備的系統上的託管分配對所有GPU都可見,可以按需遷移到任何處理器。
系統分配器
計算能力7.0的設備支持NVLink上的地址轉換服務(ATS)。ATS允許GPU直接訪問CPU的頁表。GPU MMU中的丟失將導致對CPU的地址轉換請求(ATR)。CPU在其頁表中查找該地址的虛擬到物理映射,並將轉換返回到GPU。ATS提供對系統內存的GPU完全訪問,例如分配malloc的內存、分配在堆棧上的內存、全局變量和文件備份內存。應用程序可以通過檢查pageablememoryacessuseshostpagetables屬性來查詢設備是否支持通過ATS一致地訪問可分頁內存。

	//前面介紹兩種分配Managed內存方式,
int *data = (int*)malloc(sizeof(int) * n);
kernel<<<grid, block>>>(data);

int data[1024];
kernel<<<grid, block>>>(data);

extern int *data;
kernel<<<grid, block>>>(data);

注:NVLink上的ATS目前僅在IBM Power9系統上受支持。
硬件一致性
第二代NVLink允許CPU直接加載、存儲、原子訪問每個GPU的內存。結合新的CPU控制功能,NVLink支持一致性操作,允許從GPU內存讀取的數據存儲在CPU的緩存層次結構中。從CPU緩存訪問的較低延遲是CPU性能的關鍵。計算能力6.x的設備只支持對等的GPU原子。具有計算能力7.x的設備可以通過NVLink發送GPU原子並在目標CPU上完成它們,因此第二代NVLink增加了對由GPU或CPU啓動的原子的支持。
注意,cudaMalloc分配內存不能從CPU訪問。因此,爲了利用硬件一致性,用戶必須使用統一的內存分配器,如cudaMallocManaged或具有ATS支持的系統分配器(請參閱系統分配器)。新的屬性directmanagedmeaccessfromhost指示主機是否可以在不遷移的情況下直接訪問設備上的託管內存。默認情況下,CPU訪問的cudaMallocManaged分配的駐留在GPU的內存都將觸發頁面錯誤和數據遷移。應用程序可以使用cudamemadvisesetacaccessedby性能提示和cudapudeviceid,以便在支持的系統上直接訪問GPU內存。

__global__ void write(int *ret, int a, int b) {
    ret[threadIdx.x] = a + b + threadIdx.x;
}
__global__ void append(int *ret, int a, int b) {
    ret[threadIdx.x] += a + b + threadIdx.x;
}
int main() {
    int *ret;
    cudaMallocManaged(&ret, 1000 * sizeof(int));
    cudaMemAdvise(ret, 1000 * sizeof(int), cudaMemAdviseSetAccessedBy, cudaCpuDeviceId);       // set direct access hint

    write<<< 1, 1000 >>>(ret, 10, 100);            // pages populated in GPU memory
    cudaDeviceSynchronize();
    //如果directManagedMemAccessFromHost=1,不會發生數據遷移
    //如果directManagedMemAccessFromHost=0,發生錯誤並觸發device-to-host數據遷移
    for(int i = 0; i < 1000; i++)
        printf("%d: A+B = %d\n", i, ret[i]);      
    
    //如果directManagedMemAccessFromHost=1,不會發生數據遷移
    //如果directManagedMemAccessFromHost=0,發生錯誤並觸發host-to-device數據遷移        
    append<<< 1, 1000 >>>(ret, 10, 100);   
    cudaDeviceSynchronize();                     
    cudaFree(ret); 
    return 0;
}

訪問計數器
具有計算能力7.0的設備引入了一種新的訪問計數器功能,可以跟蹤GPU對位於其他處理器上的內存的訪問頻率。訪問計數器有助於確保將內存頁移到訪問頁最頻繁的處理器的物理內存中。訪問計數器功能可以指導CPU和GPU之間以及對等GPU之間的遷移。
對於cudaMallocManaged,可以通過使用cudamemadvisesetacessedby和相應的設備ID來選擇使用訪問計數器遷移。驅動程序還可以使用訪問計數器來更有效地緩解震盪或內存超額訂閱情況。
注意:訪問計數器當前僅在IBM POWER9系統上啓用,並且僅對cudaMallocManaged分配器啓用。
編程模型
Managed memory
大多數平臺需要使用 device 和 managed 關鍵字或者使用cudaMallocManaged()函數開闢統一內存來自動管理數據。計算能力低於6.x的設備必須始終使用分配器或通過聲明全局存儲在堆上分配託管內存。不能將以前分配的內存與統一內存相關聯,也不能讓統一內存系統管理CPU或GPU堆棧指針。從CUDA 8.0開始,在具有計算能力6.x設備的支持系統上,可以使用同一指針從GPU代碼和CPU代碼訪問分配給默認OS分配器(例如malloc或new)的內存。在這些系統上,統一內存是默認的:不需要使用特殊的分配器或創建特殊管理的內存池。
一致性與併發

  1. 對於計算能力低於6.x的設備來說,不能同時訪問managed memory,因爲不能保證數據的一致性,可能GPU正在操作的時候剛好CPU訪問,會造成錯誤數據。對於計算能力6.x並且支持的設備由於引入了頁面錯誤機制所以可以同時訪問統一內存,可以查詢concurrentManagedAccess是否支持併發訪問。
__device__ __managed__ int x, y=2;
__global__  void  kernel() {
    x = 10;
}
int main() {
    kernel<<< 1, 1 >>>();
    //如果同步函數放在這裏就不會出錯
    y = 20;            // Error on GPUs not supporting concurrent access
                       
    cudaDeviceSynchronize();
    return  0;
}

對於計算能力6.x之前的架構來說,當GPU正在執行的時候,使用CPU訪問會發生段錯誤,如上代碼所示。實際上,當任何內核操作正在執行時,GPU都可以獨佔訪問所有託管數據,而不管特定的內核是否在積極地使用這些數據。從上面可以看到即使GPU使用的是x變量,CPU訪問的是y變量,訪問不同數據也會出錯。
注意,在上面的例子中,即使內核運行得很快並且在CPU接觸Y之前完成,也需要顯式同步。統一內存使用邏輯活動來確定GPU是否空閒。這與CUDA編程模型一致,CUDA編程模型指定內核可以在啓動後的任何時間運行,並且在主機發出同步調用之前,不保證已經完成。
2.邏輯上保證GPU完成其工作的任何函數調用都是有效的。這包括cudaDeviceSynchronize();cudaStreamSynchronize() and cudaStreamQuery()(前提是它返回cudaSuccess而不是cudaErrorNotReady),其中指定的流是在GPU上仍在執行的唯一流;cudaEventSynchronize()和cudaEventQuery()在指定事件後面沒有任何設備工作的情況下;以及使用記錄爲與主機完全同步的cudaMemcpy() 和cudaMemset()。
CPU從流回調中訪問託管數據是合法的,前提是GPU上沒有其他可能正在訪問託管數據的流處於活動狀態。此外,沒有任何設備工作的回調可用於同步:例如,通過從回調內部發出條件變量的信號;否則,CPU訪問僅在回調期間有效。
注意以下幾點:
o 總是允許CPU在GPU處於活動狀態時訪問非託管零拷貝數據。
o GPU在運行任何內核時都被認爲是活動的,即使該內核不使用託管數據。如果內核可能使用數據,則禁止訪問,除非設備屬性ConcurrentManagedAccess爲1。
o 除了應用於非託管內存的多GPU訪問之外,對託管內存的併發GPU訪問沒有限制。
o 對訪問託管數據的併發GPU內核沒有約束。
具體如下代碼所示

int main() {
    cudaStream_t stream1, stream2;
    cudaStreamCreate(&stream1);
    cudaStreamCreate(&stream2);
    int *non_managed, *managed, *also_managed;
    cudaMallocHost(&non_managed, 4);    // Non-managed, CPU-accessible memory
    cudaMallocManaged(&managed, 4);
    cudaMallocManaged(&also_managed, 4);
    // Point 1: CPU can access non-managed data.
    kernel<<< 1, 1, 0, stream1 >>>(managed);
    *non_managed = 1;
    // Point 2: CPU cannot access any managed data while GPU is busy,
    //          unless concurrentManagedAccess = 1
    // Note we have not yet synchronized, so "kernel" is still active.
    *also_managed = 2;      // Will issue segmentation fault
    // Point 3: Concurrent GPU kernels can access the same data.
    kernel<<< 1, 1, 0, stream2 >>>(managed);
    // Point 4: Multi-GPU concurrent access is also permitted.
    cudaSetDevice(1);
    kernel<<< 1, 1 >>>(managed);
    return  0;
}

3.之前介紹的都是GPU會佔用整個託管內存,爲了更細粒度的訪問託管內存,CUDA提供函數可以將託管內存和特定的流綁定在一起,這樣,只要這個流執行完,CPU就可以訪問,而不用管其他流是否以及完成。如果沒綁定的話,那麼整個託管內存對GPU都是可見的。

__device__ __managed__ int x, y=2;
__global__  void  kernel() {
    x = 10;
    //在內核訪問y會產生未定義
}
int main() {
    cudaStream_t stream1;
    cudaStreamCreate(&stream1);
    //將y和主機可訪問關聯在一起, 這樣做有什麼用??如果內核不能訪問了還不如開闢主機內存??
    cudaStreamAttachMemAsync(stream1, &y, 0, cudaMemAttachHost);
    cudaDeviceSynchronize();          // Wait for Host attachment to occur.
    kernel<<< 1, 1, 0, stream1 >>>(); // Note: Launches into stream1.
    y = 20;                           // Success – a kernel is running but “y” 
                                      // has been associated with no stream.
    return  0;
}

//=============================分割線==================================

__device__ __managed__ int x, y=2;
__global__  void  kernel() {
    x = 10;
}
int main() {
    cudaStream_t stream1;
    cudaStreamCreate(&stream1);
    cudaStreamAttachMemAsync(stream1, &x);// Associate “x” with stream1.
    cudaDeviceSynchronize();              // Wait for “x” attachment to occur.
    kernel<<< 1, 1, 0, stream1 >>>();     // Note: Launches into stream1.
    y = 20;                               // ERROR: “y” is still associated globally 
                                          // with all streams by default
    return  0;
}

使用cudaStreamAttachMemAsync()的主要用途是可以讓CPU線程並行執行獨立的任務。每個CPU線程創建自己的流,這樣不會造成使用默認流帶來的依賴性問題,比如如果託管內存沒有綁定特定的流,託管數據的默認全局可見性都會使多線程程序中的CPU線程之間的交互難以避免,那麼每個CPU線程就會產生依賴,這會使程序的性能下降。

	// This function performs some task, in its own private stream.
void run_task(int *in, int *out, int length) {
    // Create a stream for us to use.
    cudaStream_t stream;
    cudaStreamCreate(&stream);
    // Allocate some managed data and associate with our stream.
    // Note the use of the host-attach flag to cudaMallocManaged();
    // we then associate the allocation with our stream so that
    // our GPU kernel launches can access it.
    int *data;
    //開闢的統一內存和特定流關聯在一起,不會產生依賴
    cudaMallocManaged((void **)&data, length, cudaMemAttachHost);
    cudaStreamAttachMemAsync(stream, data);
    cudaStreamSynchronize(stream);
    // Iterate on the data in some way, using both Host & Device.
    for(int i=0; i<N; i++) {
        transform<<< 100, 256, 0, stream >>>(in, data, length);
        cudaStreamSynchronize(stream);
        host_process(data, length);    // CPU uses managed data.
        convert<<< 100, 256, 0, stream >>>(out, data, length);
    }
    cudaStreamSynchronize(stream);
    cudaStreamDestroy(stream);
    cudaFree(data);
}

在上面代碼中,cudaMallocManaged()函數指定了cudaMemAttachHost標誌,該標誌創建了一個最初對設備端執行不可見的分配(默認分配對所有流上的所有GPU內核都可見)。這確保在數據分配和綁定特定流獲取數據之間的時間間隔內不會與另一個線程的執行發生意外交互。
如果沒有這個標誌,如果由另一個線程啓動的內核恰好正在運行,則會考慮在GPU上使用新的分配。這可能會影響線程在能夠顯式地將其附加到私有流之前從CPU(例如,在基類構造函數內)訪問新分配的數據的能力。因此,爲了在線程之間實現安全的獨立性,應該進行分配來指定這個標誌。
注意:另一種方法是在分配附加到流之後,在所有線程上設置一個進程範圍的屏障。這將確保所有線程在啓動任何內核之前完成其數據/流關聯,從而避免危險。在銷燬流之前需要第二個屏障,因爲流銷燬會導致分配恢復到其默認可見性。cudaMemAttachHost標誌的存在不僅是爲了簡化這個過程,而且因爲在需要的地方不可能總是插入全局屏障。
4.由於託管內存可以從主機或設備訪問,因此cudaMemcpy*()依賴於使用cudaMemcpyKind指定的傳輸類型來確定將數據作爲主機指針或設備指針訪問。
如果使用cudaMemcpyHostTo函數,並且源數據是managed memory(源數據可以一致訪問),那麼它將從主機訪問;否則,它將從設備訪問。這同樣適用於cudaMemcpyToHost函數並且目標內存是managed memory。同理如果指定了cudaMemcpyDeviceTo並且源數據是managed memory(目標數據可以一致訪問),則將從設備訪問它。這同樣適用於cudaMemcpyToDevice()函數並且目標內存是managed memory。
如果指定了cudaMemcpyDefault,則如果無法從設備一致訪問託管數據,或者如果數據的首選位置是cudapudeviceid,並且可以從主機一致訪問託管數據,則將從主機訪問託管數據;否則,將從設備訪問它。
當對託管內存使用cudaMemset時,始終可以從設備訪問數據。數據必須可以從設備一致的訪問;否則,將返回錯誤。
當通過cudamemcpy或cudamemset從設備訪問數據時,操作流在GPU上被認爲是活動的。在此期間,如果GPU的設備屬性ConcurrentManagedAccess的值爲零,則與該流或具有全局可見性的數據關聯的任何CPU訪問都將導致段錯誤。程序必須進行適當的同步,以確保在從CPU訪問任何相關數據之前操作已經完成。
(1)爲了在給定流中從主機一致地訪問託管內存,必須至少滿足以下條件之一:
o 與給定流關聯的設備的設備屬性ConcurrentManagedAccess具有非零值。
o 內存既沒有全局可見性,也沒有與給定流關聯。(開闢的時候使用cudaMemAttachHost標誌)
(2)對於在給定流中從設備一致地訪問的託管內存,必須至少滿足以下條件之一:
o 設備的設備屬性ConcurrentManagedAccess具有非零值。
o 內存要麼具有全局可見性,要麼與給定的流相關聯。
參考鏈接
https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#um-unified-memory-programming-hd

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章