Merge pull request #2549 from SunSerega/POCGL-stables

POCGL: Обновление OpenCLABC и расширений OpenCL
This commit is contained in:
Ivan Bondarev 2021-10-24 11:20:25 +02:00 committed by GitHub
commit ba485bfd33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 4816 additions and 953 deletions

View file

@ -15,8 +15,8 @@ begin
prog['TEST'].Exec1(10, // Используем 10 ядер
// Заполняем весь массив значениями (1), прямо перед выполнением
//ToDo Разобраться почему надо KernelArg.From
KernelArg.FromCLArrayCQ(A.NewQueue.AddFill(1))
//TODO Разобраться почему надо KernelArg.From
KernelArg.FromCLArrayCQ(A.NewQueue.AddFillValue(1))
);

View file

@ -33,7 +33,7 @@ try
end, fps);
//ToDo Использовать CLArray2<byte> когда будет
//TODO Использовать CLArray2<byte> когда будет
var B := new MemorySegment(W*W*sizeof(byte));
B.WriteArray2&<byte>(MatrGen(W,W, (x,y)->byte(Random(2))));
var B_temp := new MemorySegment(B.Size);

View file

@ -15,8 +15,8 @@ begin
prog['TEST'].Exec1(10, // Используем 10 ядер
//ToDo Разобраться почему надо KernelArg.From
KernelArg.FromCLArrayCQ(A.NewQueue.AddFill(1))
//TODO Разобраться почему надо KernelArg.From
KernelArg.FromCLArrayCQ(A.NewQueue.AddFillValue(1))
);

View file

@ -1142,8 +1142,10 @@ var k := code['KernelName'];
...
var ks := code.GetAllKernels;
ks.PrintLines;
</code></pre>
<hr />
</code></pre>
</div>
<script>on_start_folder("Kernel", document.getElementById("page-11"))</script>
<div id="page-12" page_name="KernelArg" hidden=true>
<p>Методы, запускающие <code>Kernel</code> принимают специальные аргументы типа <code>KernelArg</code>, которые передаются в OpenCL-C код.<br />
Экземпляр <code>KernelArg</code> может быть создан из нескольких типов значений, а точнее:</p>
<pre><code>## uses OpenCLABC;
@ -1170,7 +1172,7 @@ k.Exec1(1,
// И указатель на размерное значение
// (в kernel попадёт само значение, не указатель)
@val2,
// Так нельзя, потому что val1 уже захвачена лямбдой из HFQ
// Так нельзя, потому что val1 была захвачена лямбдой из HFQ
// @val1,
// Расширенный набор параметров для передачи адреса
KernelArg.FromPtr(new System.IntPtr(@val2), new System.UIntPtr(sizeof(integer)))
@ -1182,9 +1184,9 @@ k.Exec1(1,
только как тонкую оптимизацию, потому что много чего может пойти не так.
Если передавать <code>@val2</code> в качестве <code>KernelArg</code> - надо знать все тонкости.</p>
<hr />
<p>Если <code>@val2</code> передали вкачестве <code>KernelArg</code>:</p>
<p>Если <code>@x</code> передали вкачестве <code>KernelArg</code>:</p>
<ol>
<li><p><code>val2</code> не может быть глобальной переменной или полем класса:</p>
<li><p><code>x</code> не может быть глобальной переменной или полем класса:</p>
<pre><code>uses OpenCLABC;
type
@ -1219,14 +1221,13 @@ end;
begin end.
</code></pre>
</li>
<li><p><code>val2</code> не должно быть захвачего лямбдой.<br />
Хотя указатель на <code>val2</code> уже можно захватывать:</p>
<li><p><code>x</code> не должно быть захвачего лямбдой.<br />
Хотя указатель на <code>x</code> уже можно захватывать:</p>
<pre><code>uses OpenCLABC;
begin
var k: Kernel;
var val1 := 3;
var val2 := 5;
// На val1 всё ещё накладываются все ограничени,
// когда val1_ptr использована в качестве KernelArg
@ -1236,7 +1237,7 @@ begin
k.Exec1(1,
val1,
HFQ(()-&gt;val1_ptr^), // захватили val1_ptr, а не val1
HFQ(()-&gt;val1_ptr^), // захватили переменную val1_ptr, а не val1
// val1 нигде не захвачена, поэтому теперь так можно
@val1,
@ -1247,19 +1248,13 @@ begin
end.
</code></pre>
</li>
<li><p>Выходить из подпрограммы, где объявили <code>val2</code> нельзя, пока <code>.Exec</code> не закончит выполнятся.
Это так же значит, что возвращать очередь, содержащую <code>KernelArg</code> из <code>@val2</code> обычно нельзя.<br />
Но это можно обойти, если объявлять переменную в другой подпрограмме, а дальше передавать только её адрес:</p>
<li><p>Выходить из подпрограммы, где объявили <code>x</code> нельзя, пока <code>.Exec</code> не закончит выполнятся.
Это так же значит, что возвращать очередь, содержащую <code>KernelArg</code> из <code>@x</code> обычно нельзя.<br />
Но это можно обойти, если объявлять переменную в другой подпрограмме:</p>
<pre><code>uses OpenCLABC;
var k: Kernel; // Вообще лучше передавать параметром в p2
// Обратите внимание - val принимает var-параметром
// То есть, p2 принимает адрес val
// (можно так же принимать указатель, но это не так удобно)
// Иначе переменная будет копироваться при передаче
// То есть без var перед параметром - val тут
// будет новой переменной, объявленной в p2 а не в p1
function p2(var val: integer): CommandQueueBase;
begin
@ -1286,18 +1281,24 @@ begin
p1;
end.
</code></pre>
<p>Тут <code>val</code> объявлена в <code>p1</code>. При этом <code>val</code> в <code>p2</code> является синонимом <code>val</code> из <code>p1</code>,
потому что объявлена <code>var</code>-параметром - передачей по ссылке.<br />
Если передать без <code>var</code> перед параметром - <code>val</code> из <code>p2</code> будет копией,
а значит перестанет существовать после выхода из <code>p2</code>.</p>
</li>
</ol>
<p>Компилятор не заставит вас следовать этим ограничениям. И программа может даже работать, игнорируя большинство сказанного тут.<br />
Но потом вы добавите что то в совсем другой части программы, или запустите её на
другом компьютере и она вдруг начнёт выводить мусорные значения, или падать с ошибками
вроде <code>AccessViolationException</code>, которые совершенно не объясняют в чём проблема.</p>
<p>Если очень интересно откуда берутся эти ограничения - начните с применения .Net декомпилятора к .exe файлам,
получаемым при компиляции программ с данной страницы. Что останется непонятно - можете спрашивать в issue.</p>
<p>Обратите внимание: Компилятор не заставит вас следовать описанным тут ограничениям.
И показанные на этой странице коды могут работать не смотря на эти ограничения,
потому что саму область памяти, на которую ссылается <code>KernelArg</code> в нём, никто никогда не уничтожает.</p>
<p>Но в то же время эту же область памяти может использовать под другие данные, таким образом
заменяя значение которое вы пытались передать в GPU на мусорные данные.</p>
<p>Это плохо в первую очередь потому, что на перезапись этих данных может повлиять совершенно не связанная
часть кода, или различие архитектур компьтеров, таким образом усложняя поиск источника ошибки.</p>
<p>Поэтому, ещё раз, используйте передачу адреса в качестве <code>KernelArg</code> только как тонкую оптимизацию и только когда понимаете что делаете.</p>
</div>
<script>on_start_folder("Kernel", document.getElementById("page-11"))</script>
<script>on_page_added(document.getElementById("page-12"))</script>
<script>on_end_folder()</script>
<div id="page-12" page_name="" hidden=true>
<div id="page-13" page_name="" hidden=true>
<p>Обычные программы невозможно запустить на GPU. Для этого надо писать особые программы.<br />
В контексте OpenCL - эти программы обычно пишутся на языке &quot;OpenCL C&quot; (основанном на языке &quot;C&quot;).</p>
<p>Язык OpenCL-C это часть библиотеки OpenCL, поэтому его справку можно найти <a href="https://www.khronos.org/registry/OpenCL/">там же</a>, где и справку OpenCL.</p>
@ -1313,8 +1314,8 @@ end.
</li>
</ul>
</div>
<script>on_start_folder("ProgramCode", document.getElementById("page-12"))</script>
<div id="page-13" page_name="Создание из исходного кода" hidden=true>
<script>on_start_folder("ProgramCode", document.getElementById("page-13"))</script>
<div id="page-14" page_name="Создание из исходного кода" hidden=true>
<p>Конструктор <code>ProgramCode</code> (<code>new ProgramCode(...)</code>) принимает тексты исходников программы на языке OpenCL-C.<br />
<strong>Именно тексты исходников, не имена файлов!</strong></p>
<p>Так же, как исходники паскаля хранят в .pas файлах, исходники OpenCL-C кода хранят в .cl файлах.
@ -1322,8 +1323,8 @@ end.
<p>Так как конструктор <code>ProgramCode</code> принимает текст - исходники программы на OpenCL-C можно хранить даже в
строке в .pas программе. Тем не менее, храненить исходники OpenCL-C кода в .cl файлах обычно удобнее всего.</p>
</div>
<script>on_page_added(document.getElementById("page-13"))</script>
<div id="page-14" page_name="Создание из бинарного файла" hidden=true>
<script>on_page_added(document.getElementById("page-14"))</script>
<div id="page-15" page_name="Создание из бинарного файла" hidden=true>
<p>После создания объекта типа <code>ProgramCode</code> из исходников можно вызвать
метод <code>ProgramCode.SerializeTo</code>, чтобы сохранить код в бинарном и прекомпилированном виде.
Обычно это делается отдельной программой (не той же самой, которая будет использовать этот бинарный код).</p>
@ -1331,10 +1332,10 @@ end.
используя статический метод <code>ProgramCode.DeserializeFrom</code>.</p>
<p>Пример можно найти в папке <code>Прекомпиляция ProgramCode</code> или <a href="https://github.com/SunSerega/POCGL/tree/master/Samples/OpenCLABC/%D0%9F%D1%80%D0%B5%D0%BA%D0%BE%D0%BC%D0%BF%D0%B8%D0%BB%D1%8F%D1%86%D0%B8%D1%8F%20ProgramCode">тут</a>.</p>
</div>
<script>on_page_added(document.getElementById("page-14"))</script>
<script>on_page_added(document.getElementById("page-15"))</script>
<script>on_end_folder()</script>
<script>on_end_folder()</script>
<div id="page-15" page_name="" hidden=true>
<div id="page-16" page_name="" hidden=true>
<p>Передавать команды для GPU по одной не эффективно.
Гораздо эффективнее передавать несколько команд сразу.</p>
<p>Для этого существуют очереди (типы, наследующие от <code>CommandQueue&lt;T&gt;</code> и <code>CommandQueueBase</code>).
@ -1353,8 +1354,8 @@ end.
</li>
</ul>
</div>
<script>on_start_folder("Очередь [команд] (CommandQueue)", document.getElementById("page-15"))</script>
<div id="page-16" page_name="" hidden=true>
<script>on_start_folder("Очередь [команд] (CommandQueue)", document.getElementById("page-16"))</script>
<div id="page-17" page_name="" hidden=true>
<p>У каждого типа-очереди есть свой тип возвращаемого значения.<br />
К примеру, так объявляется переменная в которую можно будет сохранить очередь, возвращающую <code>integer</code>:</p>
<pre><code>var Q1: CommandQueue&lt;integer&gt;;
@ -1399,9 +1400,9 @@ OtpObject( Context.Default.SyncInvoke( HPQ( ()-&gt;Print('Выполнилас
или применять операции преобразования очередей, как <code>.ThenConvert</code>,
то тип возвращаемого значения будет восприниматься как <code>Object</code>.</p>
</div>
<script>on_start_folder("Возвращаемое значение очередей", document.getElementById("page-16"))</script>
<script>on_start_folder("Возвращаемое значение очередей", document.getElementById("page-17"))</script>
<script>on_end_folder()</script>
<div id="page-17" page_name="" hidden=true>
<div id="page-18" page_name="" hidden=true>
<p>Самый простой способ выполнить очередь - вызвать метод <code>Context.SyncInvoke</code>.<br />
Он синхронно выполняет очередь и вызвращает её результат.</p>
<p>Но если надо выполнить очередь асинхронно - лучше использовать метод <code>Context.BeginInvoke</code>.<br />
@ -1436,9 +1437,9 @@ end;
</code></pre>
<p>Для этого кода есть стандартный снипет. Чтобы активировать его - напишите <code>tryo</code> и нажмите Shift+Пробел.</p>
</div>
<script>on_start_folder("Выполнение очередей", document.getElementById("page-17"))</script>
<script>on_start_folder("Выполнение очередей", document.getElementById("page-18"))</script>
<script>on_end_folder()</script>
<div id="page-18" page_name="" hidden=true>
<div id="page-19" page_name="" hidden=true>
<h1>Есть всего 10 базовых способов создать очередь:</h1>
<ol>
<li><p><a path="Из объекта с коммандами"> Из объекта с коммандами </a></p>
@ -1463,8 +1464,8 @@ end;
</li>
</ol>
</div>
<script>on_start_folder("Создание очередей", document.getElementById("page-18"))</script>
<div id="page-19" page_name="Из объекта с коммандами" hidden=true>
<script>on_start_folder("Создание очередей", document.getElementById("page-19"))</script>
<div id="page-20" page_name="Из объекта с коммандами" hidden=true>
<p>Самый просто способ создать очередь — выбрать объект, имеющий методы, соответствующие командам для GPU, и вызвав его метод <code>.NewQueue</code>.</p>
<p>Полученная очередь будет иметь особый тип, с припиской CCQ (что значит &quot;Command Container Queue&quot;, то есть очередь-контейнер для коман GPU).<br />
К примеру, метод <code>CLArray&lt;byte&gt;.NewQueue</code> вернёт очередь типа <code>CLArrayCCQ&lt;byte&gt;</code>.<br />
@ -1497,10 +1498,30 @@ Context.Default.SyncInvoke(q.AddGetArray).Println;
<pre><code>var q0: CommandQueue&lt;MemorySegment&gt;;
...
var q := new MemorySegmentCCQ(q0);
</code></pre>
</code></pre>
<hr />
<p>Команды объектов, представляющих память на GPU можно разделить на группы.</p>
<p>По направлению передачи:</p>
<ol>
<li><code>Write</code> и <code>Fill</code>: Из RAM в память GPU;</li>
<li><code>Read</code>: Из памяти GPU в RAM;</li>
<li><code>Copy</code>: Между двумя областями памяти GPU.</li>
<li><code>Get</code>: Как <code>Read</code>, но вместо использования существующей памяти RAM, выделяется новая.</li>
</ol>
<p>И по типу данных на стороне RAM:</p>
<ol>
<li><code>Data</code>: Используются данные, находящиеся по указанному адресу;</li>
<li><code>Value</code>: Используется копия указанного размерного значения;</li>
<li><code>Array</code>: Используется содержимое указанного массива размерных значений.</li>
</ol>
<p>Но при этом отсутствуют некоторые комбинации.</p>
<p>В первую очередь, в случае <code>Copy</code> нет понятия типа данных на стороне RAM, потому что RAM в принципе не задействуется.</p>
<p>Так же нет <code>ReadValue</code>, потому что нет смысла читать в копию указанного значения.<br />
Вместо него надо использовать <code>GetValue</code> - создающее и возвращающее новое размерное значение.<br />
Или <code>ReadData</code>, передавая адрес значения, которое надо перезаписать - но с этим надо осторожно, как и в случае <a path="../../Простые обёртки/Kernel/KernelArg"> <code>KernelArg</code> </a>.</p>
</div>
<script>on_page_added(document.getElementById("page-19"))</script>
<div id="page-20" page_name="Из готового результата" hidden=true>
<script>on_page_added(document.getElementById("page-20"))</script>
<div id="page-21" page_name="Из готового результата" hidden=true>
<p>Переменной очереди можно присвоить значение, тип которого совпадает с возвращаемым значением очереди:</p>
<pre><code>var q: CommandQueue&lt;integer&gt; := 5;
</code></pre>
@ -1520,8 +1541,8 @@ var q := new MemorySegmentCCQ(q0);
Writeln($'Очередь не константная');
</code></pre>
</div>
<script>on_page_added(document.getElementById("page-20"))</script>
<div id="page-21" page_name="С кодом для CPU" hidden=true>
<script>on_page_added(document.getElementById("page-21"))</script>
<div id="page-22" page_name="С кодом для CPU" hidden=true>
<p>Иногда между командами для GPU надо вставить выполнение обычного кода на CPU.
А разрывать для этого очередь на две части - плохо, потому что
одна целая очередь всегда выполнится быстрее двух её частей.</p>
@ -1584,8 +1605,8 @@ end.
Даже потокобезопастный код, как вызов <code>Buffer.Dispose</code>, может привести к утечкам памяти, если посреди него кинут <code>ThreadAbortException</code>.</p>
<p>Считайте получение <code>ThreadAbortException</code> критической ошибкой, после которой очень желателен перезапуск всего .exe файла.</p>
</div>
<script>on_page_added(document.getElementById("page-21"))</script>
<div id="page-22" page_name="Комбинируя другие очереди" hidden=true>
<script>on_page_added(document.getElementById("page-22"))</script>
<div id="page-23" page_name="Комбинируя другие очереди" hidden=true>
<p>Если сложить две очереди A и B (<code>var C := A+B</code>) — получится очередь C, в которой сначала выполнится A, а затем B.<br />
Очередь C будет считаться выполненной тогда, когда выполнится очередь B.</p>
<p>Если умножить две очереди A и B (<code>var C := A*B</code>) — получится очередь C, в которой одновременно начнут выполняться A и B.<br />
@ -1653,8 +1674,8 @@ begin
end.
</code></pre>
</div>
<script>on_page_added(document.getElementById("page-22"))</script>
<div id="page-23" page_name="Из очереди + преобразователя" hidden=true>
<script>on_page_added(document.getElementById("page-23"))</script>
<div id="page-24" page_name="Из очереди + преобразователя" hidden=true>
<p>Если надо с минимальными затратами изменить представление компилятора об очереди - лучше всего использовать <code>.Cast</code>.<br />
Но он ограничен примерно так же, как метод последовательностей <code>.Cast</code>. То есть:</p>
<pre><code>uses OpenCLABC;
@ -1702,12 +1723,12 @@ begin
end.
</code></pre>
</div>
<script>on_page_added(document.getElementById("page-23"))</script>
<div id="page-24" page_name="Из повторения очереди" hidden=true>
<script>on_page_added(document.getElementById("page-24"))</script>
<div id="page-25" page_name="Из повторения очереди" hidden=true>
<p><del>В данный момент всё ещё не работает... Но уже совсем скоро, правда-правда!</del></p>
</div>
<script>on_page_added(document.getElementById("page-24"))</script>
<div id="page-25" page_name="Множественное использование очереди" hidden=true>
<script>on_page_added(document.getElementById("page-25"))</script>
<div id="page-26" page_name="Множественное использование очереди" hidden=true>
<p>Одну и ту же очередь можно использовать несколько раз, в том числе одновременно:</p>
<pre><code>uses OpenCLABC;
@ -1790,8 +1811,8 @@ end.
<p>А если контекст разный - надо сохранять результат в переменную и использовать <code>Wait</code> очереди
(<a path="Из ожидания очередей">подробнее на странице ниже</a>).</p>
</div>
<script>on_page_added(document.getElementById("page-25"))</script>
<div id="page-26" page_name="Особые .Add методы" hidden=true>
<script>on_page_added(document.getElementById("page-26"))</script>
<div id="page-27" page_name="Особые .Add методы" hidden=true>
<p>Между командами в <code>CCQ</code> очередях бывает надо вставить выполнение другой очереди или кода для CPU.</p>
<p>Это можно сделать, используя несколько <code>.NewQueue</code>:</p>
<pre><code>var s: MemorySegment;
@ -1824,8 +1845,8 @@ q.AddQueue(q);
<p>Все <code>.Add*</code> методы добавляют команды в существующую очередь, а не создают новую.
Поэтому очередь, созданная предыдущим кодом при попытке выполнения начнёт циклически запускать саму себя.</p>
</div>
<script>on_page_added(document.getElementById("page-26"))</script>
<div id="page-27" page_name="Из ожидания очередей" hidden=true>
<script>on_page_added(document.getElementById("page-27"))</script>
<div id="page-28" page_name="Из ожидания очередей" hidden=true>
<p>Большинство деревьев выполнения очередей можно реализовать используя только сложение и умножение очередей. Но есть несколько проблем:</p>
<ul>
<li><p>Большинство но не все. Пример дерева которое нельзя реализовать через сложение и умножение можно найти <a href="https://github.com/SunSerega/POCGL/blob/master/Samples/OpenCLABC/Wait%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8/1.pas">тут</a> или в файле:<br />
@ -2023,8 +2044,8 @@ t1.Wait;
t2.Wait;
</code></pre>
</div>
<script>on_page_added(document.getElementById("page-27"))</script>
<div id="page-28" page_name="Не создавая явно" hidden=true>
<script>on_page_added(document.getElementById("page-28"))</script>
<div id="page-29" page_name="Не создавая явно" hidden=true>
<p>Передавать команды по одной, когда их несколько - ужасно не эффективно!<br />
Но нередко бывает так, что команда всего одна. Или для отладки надо одноразово выполнить одну команду.</p>
<p>Для таких случаев можно создавать очередь неявно:<br />
@ -2055,9 +2076,9 @@ a[2] := 7;
a.GetArray.Println;
</code></pre>
</div>
<script>on_page_added(document.getElementById("page-28"))</script>
<script>on_page_added(document.getElementById("page-29"))</script>
<script>on_end_folder()</script>
<div id="page-29" page_name="" hidden=true>
<div id="page-30" page_name="" hidden=true>
<p>Все методы создающие одну команду (<code>*CCQ.Add*</code> методы и все методы неявных очередей)
могут принимать очередь вместо значения в качестве любого параметра. Но в таком случае
возвращаемый тип очереди должен совпадать с типом параметра. К примеру:</p>
@ -2085,10 +2106,10 @@ a.GetArray.Println;
<p>Обычно вложенные очереди используются при вызове kernel'а,
когда надо записать что-то в память GPU прямо перед вызовом kernel'а.</p>
</div>
<script>on_start_folder("Вложенные очереди", document.getElementById("page-29"))</script>
<script>on_start_folder("Вложенные очереди", document.getElementById("page-30"))</script>
<script>on_end_folder()</script>
<script>on_end_folder()</script>
<div id="page-30" page_name="" hidden=true>
<div id="page-31" page_name="" hidden=true>
<p>В данной справке в нескольких местах можно встретить утверждения вроде</p>
<blockquote>
<p>Вызовы <code>Context.BeginInvoke</code> стоит, по возможности, объединять.</p>
@ -2103,8 +2124,8 @@ a.GetArray.Println;
<li><a path="Внутренности BeginInvoke"> Что делает вызов <code>BeginInvoke</code> </a></li>
</ul>
</div>
<script>on_start_folder("Оптимизация", document.getElementById("page-30"))</script>
<div id="page-31" page_name="Внутренности BeginInvoke" hidden=true>
<script>on_start_folder("Оптимизация", document.getElementById("page-31"))</script>
<div id="page-32" page_name="Внутренности BeginInvoke" hidden=true>
<p>Кроме самого выполнения очередей - им так же необходима инициализация и финализация.</p>
<hr />
<h3>Инициализация</h3>
@ -2144,7 +2165,7 @@ c1.BeginInvoke(A);
<p>Внутренние оптимизации <code>OpenCLABC</code> делают этот код практически не отличимым по скорости, от <code>BeginInvoke(A+B)</code>.</p>
<p>Единственное различие - время инициализации. Потому что <code>A</code> не запустится, пока не закончится обход дерева <code>c2.BeginInvoke</code>.</p>
</div>
<script>on_page_added(document.getElementById("page-31"))</script>
<script>on_page_added(document.getElementById("page-32"))</script>
<script>on_end_folder()</script>
<script>on_end_folder()</script>
</body>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -326,7 +326,7 @@ type
public function ToString: string; override := $'x_colormap[{val}]';
end;
//ToDo что это вообще за типы? я не могу найти ничего про них
//TODO что это вообще за типы? я не могу найти ничего про них
x_vl_server = record
public val: IntPtr;
public constructor(val: IntPtr) := self.val := val;