Ricerca…


introduzione

Prima di utilizzare OpenCL, è necessario impostare il proprio codice per utilizzarlo. Questo argomento si concentra su come ottenere opencl up e in esecuzione nel progetto ed eseguire un kernel di base. Gli esempi sono basati sul wrapper C # OpenCL.NET ma dal momento che il wrapper non aggiunge astrazione ad OpenCL, il codice verrà probabilmente eseguito con pochissime modifiche anche su C / C ++.

Le chiamate in C # possono avere il seguente aspetto: "Cl.GetPlatformIDs". Per C-Style OpenCL Api chiameresti "clGetPlatformIDs" e per lo stile C ++ uno "cl :: GetPlatformIDs"

Osservazioni

  • NVidia, AMD e Intel hanno implementazioni leggermente diverse di OpenCL ma le differenze note sono (per la mia esperienza) limitate ai requisiti di parentesi e ai cast impliciti. A volte NVidia blocca il tuo kernel mentre prova a capire il sovraccarico corretto per un metodo. In questo caso aiuta a offrire un cast esplicito per aiutare la GPU. Il problema è stato osservato per i kernel compilati a runtime.

  • Per ottenere maggiori informazioni sulle chiamate utilizzate in questo argomento, è sufficiente google 'OpenCL' seguito dal nome della funzione. Il gruppo Khronos ha una documentazione completa su tutti i parametri e i tipi di dati disponibili sul loro sito web.

Inizializzazione del dispositivo di destinazione

I kernel OpenCL possono essere eseguiti sulla GPU o sulla CPU. Ciò consente soluzioni di fallback, in cui il cliente può disporre di un sistema molto obsoleto. Il programmatore può anche scegliere di limitare la propria funzionalità alla CPU o alla GPU.

Per iniziare a utilizzare OpenCL, avrai bisogno di un "Contesto" e un "Dispositivo". Entrambe sono strutture definite dall'API OpenCL (nota anche come cl :: Context o clContext & ~ Device) e definiscono il processore di destinazione utilizzato.

Per ottenere il tuo dispositivo e il contesto, è necessario interrogare un elenco di piattaforme disponibili, ognuna delle quali può ospitare più dispositivi. Una piattaforma rappresenta la tua GPU fisica e CPU, mentre un dispositivo può ulteriormente distinguere le unità di calcolo contenute. Per le GPU, la maggior parte delle piattaforme avrà un solo dispositivo. Ma una CPU può offrire una GPU integrata aggiuntiva oltre alle sue capacità della CPU.

Il contesto gestisce la memoria, le code dei comandi, i diversi kernel e programmi. Un contesto può essere limitato a un singolo dispositivo ma anche fare riferimento a più dispositivi.

Una nota API rapida prima di iniziare la codifica: quasi tutte le chiamate a OpenCL forniscono un valore di errore, come valore di ritorno o tramite un valore di riferimento (puntatore in C). Ora iniziamo.

ErrorCode err;
var platforms = Cl.GetPlatformIDs(out err);
if(!CheckError(err, "Cl.GetPlatformIDs")) return;
foreach (var platform in platforms) {
    foreach (var device in Cl.GetDeviceIDs(platform, DeviceType.Gpu, out err)) {
        if(!CheckError(err, "Cl.GetDeviceIDs")) continue;
        [...]
    }
}

Questo snippet di codice interroga tutti i dispositivi GPU disponibili sul sistema. Ora puoi aggiungerli a un elenco o avviare il tuo contesto direttamente con la prima partita. La funzione 'CheckError (...)' è una semplice utility, che controlla se il codice di errore ha il valore di successo o uno diverso e può offrirti qualche registrazione. Si consiglia di utilizzare una funzione o una macro separata, perché la chiamerai molto.

ErrorCode è solo un enum sul tipo di dati cl_int per C #, C / C ++ può confrontare il valore int con costanti di errore predefinite come elencato qui: https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/ xhtml / errors.html

Potresti anche voler controllare se il dispositivo supporta tutte le funzionalità necessarie, altrimenti i tuoi kernel potrebbero bloccarsi in fase di runtime. Puoi interrogare una capacità del dispositivo con

Cl.GetDeviceInfo(_device, DeviceInfo.ImageSupport, out err)

Questo esempio chiede al dispositivo se può eseguire funzioni di immagine. Per il prossimo e ultimo passo, dobbiamo costruire il nostro contesto fuori dai dispositivi raccolti.

_context = Cl.CreateContext(null, 1, new[] { _device }, ContextNotify, IntPtr.Zero, out err);

Alcune cose stanno succedendo qui. Per gli utenti di C / C ++, IntPtr è un indirizzo puntatore in C #. Mi concentrerò sulle parti importanti qui.

  • Il secondo parametro definisce il numero di dispositivi che si desidera utilizzare
  • Il terzo parametro è un array di quei dispositivi (o un puntatore in C / C ++)
  • E il terzo parametro è un puntatore a funzione per una funzione di callback. Questa funzione verrà utilizzata ogni volta che si verificano errori nel contesto.

Per l'utilizzo futuro, dovrai conservare i tuoi dispositivi usati e il contesto da qualche parte.

Dopo aver completato tutte le interazioni OpenCL, dovrai rilasciare nuovamente il contesto con

Cl.ReleaseContext(_context);

Compilare il tuo kernel

I kernel possono essere compilati in fase di esecuzione sul dispositivo di destinazione. Per fare ciò, è necessario

  • il codice sorgente del kernel
  • il dispositivo di destinazione su cui compilare
  • un contesto creato con il dispositivo di destinazione

Un rapido aggiornamento della terminologia: un programma contiene una raccolta di kernel. Puoi pensare ad un programma come un file sorgente C / C ++ / C # completo, mentre i kernel sono i diversi membri di funzione di quel file.

Per prima cosa dovrai creare un programma dal tuo codice sorgente.

var program = Cl.CreateProgramWithSource(_context, 1, new[] { source }, null, out err);

È possibile combinare più file sorgente in un unico programma e compilarli insieme, il che consente di avere kernel in file diversi e di raggrupparli insieme in un unico passaggio.

Nel prossimo passaggio dovrai compilare il programma sul tuo dispositivo di destinazione.

err = Cl.BuildProgram(program, 1, new[] { _device }, string.Empty, null, IntPtr.Zero);

Ora ecco un piccolo avvertimento: il codice di errore ti dice solo se la chiamata alla funzione stessa ha avuto successo, ma non se il tuo codice è stato effettivamente compilato. Per verificarlo, dobbiamo interrogare alcune informazioni aggiuntive

BuildStatus status;
status = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Status, out err).CastTo<BuildStatus>();
if (status != BuildStatus.Success) {
    var log = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Log, out err);
}

Le persone C / C ++ possono ignorare il cast alla fine e confrontare il numero intero restituito con la costante corrispondente.

La prima chiamata verifica se la nostra build fosse effettivamente riuscita. Altrimenti possiamo recuperare un log e vedere esattamente dove le cose sono andate storte. Vedere le osservazioni per alcuni pitfals comuni riguardanti diverse piattaforme.

Una volta che il programma è stato creato, è necessario estrarre i diversi kernel dal programma compilato. Per farlo, crea i tuoi kernel con

_kernel = Cl.CreateKernel(_program, kernel, out err);

dove 'kernel' è una stringa del nome del kernel. Quando hai finito con il tuo kernel, devi rilasciarlo con

Cl.ReleaseKernel(_kernel);

Creazione di una coda comandi

Per avviare qualsiasi operazione sui tuoi dispositivi, avrai bisogno di una coda di comando per ogni dispositivo. La coda tiene traccia delle diverse chiamate che hai fatto al dispositivo di destinazione e le mantiene in ordine. La maggior parte dei comandi può anche essere eseguita in modalità di blocco o non bloccante.

Creare una coda è piuttosto semplice:

_queue = Cl.CreateCommandQueue(_context, _device, CommandQueueProperties.None, out err);

L'interazione di base con la coda dei comandi consiste nell'accodare le diverse operazioni che si desidera eseguire, ad esempio copiare i dati da e verso il dispositivo e avviare un kernel.

Quando hai finito di usare la coda dei comandi, devi rilasciare la coda con una chiamata a

Cl.ReleaseCommandQueue(_queue);

Esecuzione del kernel

Quindi ora arriviamo alle cose reali, eseguendo i kernel sul dispositivo parallelo. Leggere le nozioni di base sull'hardware per comprendere appieno il dispatching del kernel.

Per prima cosa è necessario impostare gli argomenti del kernel prima di chiamare effettivamente il kernel. Questo è fatto tramite

err = Cl.SetKernelArg(_kernel, $argumentIndex, $argument);

Se non si imposta ogni argomento prima di avviare il kernel, il kernel fallirà.

Prima di avviare il nostro kernel, dobbiamo calcolare "dimensione del lavoro globale" e "dimensione del lavoro locale".

la dimensione di lavoro globale è il numero totale di thread che verranno lanciati sulla tua GPU. La dimensione del lavoro locale è il numero di thread all'interno di ciascun blocco di thread. Le dimensioni del lavoro locale possono essere omesse se il kernel non necessita di requisiti speciali. Ma se viene fornita la dimensione del lavoro locale, la dimensione del lavoro globale deve essere un multiplo della dimensione del lavoro locale.

Le dimensioni del lavoro possono essere monodimensionali, bidimensionali o tridimensionali. La scelta su quante dimensioni vuoi dipende interamente da te e puoi scegliere quello che meglio si adatta al tuo algoritmo.

Ora che abbiamo deciso le dimensioni del nostro lavoro, possiamo chiamare il kernel.

Event clevent;
err = Cl.EnqueueNDRangeKernel(_queue, _kernel, $dimensions, null, $globalWorkSize, $localWorkSize, 0, null, out clevent);

Le $ size definiscono il numero desiderato di dimensioni, $ globalWorkSize è una matrice di dimensioni $ dimensioni con la dimensione di lavoro globale e lo stesso per $ localWorkSize. L'ultimo argomento ti fornisce un oggetto che rappresenta il tuo kernel attualmente eseguito.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow