Skip to content

Commit 3ba3c3f

Browse files
rrtheonlyonemartin-henz
andauthoredApr 29, 2020
Add Source §4 GPU (#576)
* Add GPU specific code as a folder * Call GPU code in transpile + update global symbols to include new function * Add new dependency - gpu.js * Update snapshots * Tweak and upgrade gpu folder * Add new functions to astcreator * Modify Transpiler to work with gpu * Fix bug in external variable evaluation * Add runtime checks to make sure array is initialized * run code manually under certain conditions * minor bug fixes - array check + loc undefined error * Update external variable lookup * Bug fixes and optimizations to handle library * Update snapshots in test * Update loop condition check * Add new variant : source 4 GPU + update tests * Add display statements to program when the GPU is used * Improve display statement message * Update snapshots in test * Add documentation for source gpu * Remove __createKernal if variant is not GPU + update snapshots * Update lock file * Add postive + negative tests for gpu code * Improve display statement message * Improve latex documentation * Improve latex documentation * Update tests * Revert change to stringify tests * Add more tests + fix bugs * Update latex docs * Update documentation + bug fixes * Add failing test: empty for loop * Fix failing test * Add failing test: __createKernel used in program * Fix failing test: get unique name * Remove concurrency library from docs Co-authored-by: martin-henz <[email protected]>
1 parent eb5fd65 commit 3ba3c3f

20 files changed

+4282
-1549
lines changed
 

‎docs/md/README_4_GPU.md

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
Source §4 GPU is a small programming language, designed to allow users to accelerate their programs
2+
by making use of their GPUs!
3+
4+
## What names are predeclared in Source §4 GPU?
5+
6+
On the right, you see all predeclared names of Source §4, in alphabetical
7+
order. Click on a name to see how it is used. They come in these two groups:
8+
<ul>
9+
<li>
10+
<a href="../MISC/index.html">MISC</a>: Miscellaneous constants and functions
11+
</li>
12+
<li>
13+
<a href="../MATH/index.html">MATH</a>: Mathematical constants and functions
14+
</li>
15+
<li>
16+
<a href="../LISTS/index.html">LISTS</a>: Support for lists
17+
</li>
18+
<li>
19+
<a href="../PAIRMUTATORS/index.html">PAIRMUTATORS</a>: Mutating pairs
20+
</li>
21+
<li>
22+
<a href="../ARRAYS/index.html">ARRAYS</a>: Support for arrays
23+
</li>
24+
<li>
25+
<a href="../STREAMS/index.html">STREAMS</a>: Support for streams
26+
</li>
27+
<li>
28+
<a href="../MCE/index.html">MCE</a>: Support for the meta-circular evaluator
29+
</li>
30+
</ul>
31+
32+
The <a href="https://sourceacademy.nus.edu.sg">Source Academy</a>,
33+
a learning environment that uses SICP JS and Source, comes with the following
34+
<a href="External libraries/">external libraries</a>:
35+
<ul>
36+
<li>
37+
<a href="../RUNES/index.html">RUNES</a>: Library for runes graphics
38+
</li>
39+
<li>
40+
<a href="../CURVES/index.html">CURVES</a>: Library for curve graphics
41+
</li>
42+
<li>
43+
<a href="../SOUNDS/index.html">SOUNDS</a>: Library for sound processing
44+
</li>
45+
<li>
46+
<a href="../BINARYTREES/index.html">BINARYTREES</a>: Library for binary trees
47+
</li>
48+
<li>
49+
<a href="../PIX%26FLIX/index.html">PIX&amp;FLIX</a>: image and video processing
50+
</li>
51+
</ul>
52+
53+
## What can you do in Source §4 GPU?
54+
55+
You can write for loops as per normal source and if certain conditions are met, the GPU will
56+
be invoked to run the program!
57+
58+
### Example:
59+
60+
```=javascript
61+
const size = 100;
62+
63+
const L = [];
64+
const R = [];
65+
for (let r = 0; r < size; r = r + 1) {
66+
L[r] = [];
67+
R[r] = [];
68+
for (let c = 0; c < size; c = c + 1) {
69+
L[r][c] = r*c;
70+
R[r][c] = r + c;
71+
}
72+
}
73+
74+
const res = [];
75+
for (let r = 0; r < size; r = r + 1) {
76+
res[r] = [];
77+
}
78+
79+
const startTime = runtime();
80+
for (let r = 0; r < size; r = r + 1) {
81+
for (let c = 0; c < size; c = c + 1) {
82+
let sum = 0;
83+
for (let i = 0; i < size; i = i + 1) {
84+
sum = sum + L[r][i] * R[i][c];
85+
}
86+
res[r][c] = sum;
87+
}
88+
}
89+
90+
const endTime = runtime();
91+
const elapsed = endTime - startTime;
92+
93+
display(res);
94+
display(elapsed, "Time taken: ");
95+
```
96+
97+
## You want the definitive specs?
98+
99+
For our development team, we are maintaining a definitive description
100+
of the language, called the
101+
<a href="../source_4_gpu.pdf">Specification of Source §4 GPU</a>. Feel free to
102+
take a peek!
103+
104+

‎docs/md/README_top.md

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
## <a href="source_4/">Source §4</a>
2929

30+
## <a href="source_4_gpu/">Source §4 GPU</a>
31+
3032
## <a href="External libraries/">External Libraries</a>
3133

3234
The <a href="https://sourceacademy.nus.edu.sg">Source Academy</a>,

‎docs/specs/source_4_gpu.tex

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
\input source_header.tex
2+
3+
\begin{document}
4+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5+
\docheader{2021}{Source}{\S 4 GPU}{Martin Henz, Rahul Rajesh, Zhang Yuntong}
6+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
7+
8+
\input source_intro.tex
9+
10+
\section*{Changes}
11+
12+
Source \S 4 GPU allows for Source programs to be accelerated on the GPU if certain conditions are met.
13+
The exact specifications for this is outlined on page \pageref{gpu_supp}. Source \S 4 GPU defines a formal specification
14+
to identify areas in the program that are embarrssingly parallel (e.g. for loops etc.) . These will then
15+
be run in parallel across GPU threads. Experimentation has shown that Source \S 4 GPU is orders of magnitude faster
16+
than Source \S 4 for heavy CPU bound tasks (matrix multiplication of large matrices)
17+
18+
\input source_bnf.tex
19+
20+
\input source_3_bnf.tex
21+
22+
\newpage
23+
24+
\input source_boolean_operators
25+
26+
\input source_loops
27+
28+
\input source_return_3
29+
30+
\input source_names
31+
32+
\input source_lists
33+
34+
\input source_pair_mutators
35+
36+
\input source_array_support
37+
38+
\input source_streams
39+
40+
\input source_numbers
41+
42+
\input source_strings
43+
44+
\input source_arrays
45+
46+
\input source_typing_3
47+
48+
\input source_interpreter
49+
50+
\input source_comments
51+
52+
\input source_js_differences
53+
54+
\newpage
55+
56+
\section*{GPU Acceleration}
57+
\label{gpu_supp}
58+
This section outlines the specifications for programs to be accelerated using the GPU.\
59+
\input source_gpu_bnf.tex
60+
61+
\newpage
62+
63+
\section*{Restrictions}
64+
65+
Even if the BNF syntax is met, GPU acceleration can only take place if all the restrictions below are satisfied. If all criteria are met, the \textit{gpu\_statement} loops are embarrassingly parallel.
66+
67+
\subsection*{Special For Loops}
68+
69+
In the BNF, we have special loops that take on this form:
70+
\begin{alignat*}{9}
71+
&& \textbf{\texttt{for}}\ \textbf{\texttt{(}}
72+
\ \textit{gpu\_for\_let} \textbf{\texttt{;}} \\
73+
&& \ \ \textit{gpu\_condition} \textbf{\texttt{;}} \\
74+
&& \textit{gpu\_for\_assignment} \ \textbf{\texttt{)}}
75+
\end{alignat*}
76+
77+
These are the loops that will be taken into consideration for parallelization. However, on top of the BNF syntax, the below requirement must also be statisfied:
78+
79+
\begin{itemize}
80+
\item{the names declared in each \textit{gpu\_for\_let} have to be different across the loops}
81+
\item{in each loop, the \textit{gpu\_condition} and the \textit{gpu\_for\_assignment} must use the name declared
82+
in the respective \textit{gpu\_for\_let} statement}
83+
\end{itemize}
84+
85+
\subsection*{GPU Function}
86+
87+
A \textit{gpu\_function} has to be a \textit{math\_\texttt{*}} function
88+
89+
\subsection*{Core Statement}
90+
91+
Within \textit{core\_statement}, there are some constraints:
92+
93+
\begin{itemize}
94+
\item{no assignment to any global variables (all assignments can only be done to variables defined in the \textit{gpu\_block}})
95+
\item{no use of the variable in \textit{gpu\_result\_assignment} at an offset from the current index e.g. cannot be i - 1}
96+
\end{itemize}
97+
98+
\subsection*{GPU Result Statement}
99+
100+
The \textit{gpu\_result\_assignment} is the statement that stores a value calculated in core statements into a result array.
101+
It access an array at a certain coordinate e.g. ${array[{i_1}][{i_2}][{i_3}]}$. For this:
102+
103+
\begin{itemize}
104+
\item{This result array has to be defined outside the \textit{gpu\_block}.}
105+
\item{The sequence of coordinates which we access in the result array ${{i_1}, {i_2}, {i_3} ... i_{k}}$ must be a
106+
prefix of the special for loop counters ${[c_1,c_2 ... c_n]}$.}
107+
\item{ If you have ${n}$ special for loops, the array expression can take on ${k}$ coordinates where ${0 < k \leq n}$.
108+
The order matters as well, it has to follow the same order as the special for loops: you cannot have ${name[c_2][c_1]}$.}
109+
\end{itemize}
110+
111+
\section*{Examples}
112+
113+
Below are some examples of valid and invalid source gpu programs:\\
114+
115+
\textbf{Valid} - Using first loop counter. (meaning the loop will be run across N threads; the first loop is parallelized away):
116+
\begin{verbatim}
117+
for (let i = 0; i < N; i = i + 1) {
118+
for (let k = 0; k < M; k = k + 1) {
119+
res[i] = arr[k % 2] + 1;
120+
}
121+
}
122+
\end{verbatim}
123+
124+
\textbf{Invalid} - Counter used is not a prefix of for loop counters:
125+
\begin{verbatim}
126+
for (let i = 0; i < N; i = i + 1) {
127+
for (let k = 0; k < M; k = k + 1) {
128+
res[k] = arr[i % 2] + 1;
129+
}
130+
}
131+
\end{verbatim}
132+
133+
\textbf{Valid} - Using first three loop counters (meaning the loop will be run across N*M*C threads, if available):
134+
\begin{verbatim}
135+
for (let i = 0; i < N; i = i + 1) {
136+
for (let j = 0; j < M; j = j + 1) {
137+
for (let k = 0; k < C; k = k + 1) {
138+
let x = math_pow(2, 10);
139+
let y = x * (1000);
140+
arr[i][j][k] = (x + y * 2);
141+
}
142+
}
143+
}
144+
\end{verbatim}
145+
146+
\textbf{Invalid} - Indices are in wrong order (must respect for loop counter orders):
147+
\begin{verbatim}
148+
for (let i = 0; i < N; i = i + 1) {
149+
for (let j = 0; j < M; j = j + 1) {
150+
for (let k = 0; k < C; k = k + 1) {
151+
let x = math_pow(2, 10);
152+
let y = x * (1000);
153+
res[k][j][i] = (x + y * 2);
154+
}
155+
}
156+
}
157+
\end{verbatim}
158+
159+
\textbf{Invalid} - Using an index that is not part of a special for loop (see above):
160+
\begin{verbatim}
161+
for (let i = 0; i < N; i = i + 1) {
162+
for (let j = 0; j < M; j = j + 1) {
163+
for (let k = 1; k < C; k = k + 2) {
164+
res[k] = arr1[i] + arr2[j];
165+
}
166+
}
167+
}
168+
\end{verbatim}
169+
170+
\newpage
171+
172+
\input source_list_library
173+
174+
\newpage
175+
176+
\input source_stream_library
177+
178+
179+
\end{document}

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"lodash": "^4.17.13",
2727
"node-getopt": "^0.3.2",
2828
"source-map": "^0.7.3",
29-
"xmlhttprequest-ts": "^1.0.1"
29+
"xmlhttprequest-ts": "^1.0.1",
30+
"gpu.js": "^2.9.3"
3031
},
3132
"main": "dist/index",
3233
"types": "dist/index",

‎scripts/jsdoc.sh

+13
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ run() {
136136
${LIB}/array.js \
137137
${LIB}/pairmutator.js \
138138
${LIB}/mce.js
139+
140+
# Source §4 GPU
141+
142+
${JSDOC} -r -t ${TMPL} \
143+
-c docs/jsdoc/conf.json \
144+
-R ${MD}/README_4_GPU.md \
145+
-d ${DST}/"source_4_gpu"/ \
146+
${LIB}/misc.js \
147+
${LIB}/math.js \
148+
${LIB}/list.js \
149+
${LIB}/stream.js \
150+
${LIB}/array.js \
151+
${LIB}/pairmutator.js
139152

140153
# MISC
141154

‎src/__tests__/stringify.ts

+232-232
Large diffs are not rendered by default.

‎src/createContext.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as stream from './stdlib/stream'
1212
import { streamPrelude } from './stdlib/stream.prelude'
1313
import { Context, CustomBuiltIns, Value, Variant } from './types'
1414
import * as operators from './utils/operators'
15+
import * as gpu_lib from './gpu/lib'
1516
import { stringify } from './utils/stringify'
1617
import { lazyListPrelude } from './stdlib/lazyList.prelude'
1718
export class LazyBuiltIn {
@@ -59,7 +60,8 @@ export const createEmptyContext = <T>(
5960
}
6061
const length = GLOBAL[GLOBAL_KEY_TO_ACCESS_NATIVE_STORAGE].push({
6162
globals: { variables: new Map(), previousScope: null },
62-
operators: new Map(Object.entries(operators))
63+
operators: new Map(Object.entries(operators)),
64+
gpu: new Map(Object.entries(gpu_lib))
6365
})
6466
return {
6567
chapter,

‎src/gpu/__tests__/noTranspile.ts

+257
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { mockContext } from '../../mocks/context'
2+
import { parse } from '../../parser/parser'
3+
import { stripIndent } from '../../utils/formatters'
4+
import { transpile } from '../../transpiler/transpiler'
5+
6+
test('empty for loop does not get transpiled', () => {
7+
const code = stripIndent`
8+
for (let i = 0; i < 10; i = i + 1) {}
9+
`
10+
const context = mockContext(4, 'gpu')
11+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
12+
.transpiled
13+
14+
const cnt = transpiled.match(/__createKernel/g)?.length
15+
expect(cnt).toEqual(2)
16+
})
17+
18+
test('simple for loop with different update does not get transpiled', () => {
19+
const code = stripIndent`
20+
let res = [];
21+
for (let i = 0; i < 5; i = i + 2) {
22+
res[i] = i;
23+
}
24+
`
25+
const context = mockContext(4, 'gpu')
26+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
27+
.transpiled
28+
29+
const cnt = transpiled.match(/__createKernel/g)?.length
30+
expect(cnt).toEqual(2)
31+
})
32+
33+
test('simple for loop with different loop variables does not get transpiled', () => {
34+
const code = stripIndent`
35+
let res = [];
36+
let j = 0;
37+
for (let i = 0; j < 5; j = j + 1) {
38+
res[i] = i;
39+
}
40+
`
41+
const context = mockContext(4, 'gpu')
42+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
43+
.transpiled
44+
45+
const cnt = transpiled.match(/__createKernel/g)?.length
46+
expect(cnt).toEqual(2)
47+
})
48+
49+
test('simple for loop with const initialization does not get transpiled', () => {
50+
const code = stripIndent`
51+
let res = [];
52+
let j = 0;
53+
for (const i = 0; i < 5; i = i + 1) {
54+
res[i] = i;
55+
}
56+
`
57+
const context = mockContext(4, 'gpu')
58+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
59+
.transpiled
60+
61+
const cnt = transpiled.match(/__createKernel/g)?.length
62+
expect(cnt).toEqual(2)
63+
})
64+
65+
test('simple for loop with non-zero initialization does not get transpiled', () => {
66+
const code = stripIndent`
67+
let res = [];
68+
for (let i = 1; i < 5; i = i + 1) {
69+
res[i] = i;
70+
}
71+
`
72+
const context = mockContext(4, 'gpu')
73+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
74+
.transpiled
75+
76+
const cnt = transpiled.match(/__createKernel/g)?.length
77+
expect(cnt).toEqual(2)
78+
})
79+
80+
test('simple for loop with a function end counter does not get transpiled', () => {
81+
const code = stripIndent`
82+
let res = [];
83+
let f = () => 5;
84+
for (let i = 1; i < f(); i = i + 1) {
85+
res[i] = i;
86+
}
87+
`
88+
const context = mockContext(4, 'gpu')
89+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
90+
.transpiled
91+
92+
const cnt = transpiled.match(/__createKernel/g)?.length
93+
expect(cnt).toEqual(2)
94+
})
95+
96+
test('simple for loop with different initialization does not get transpiled', () => {
97+
const code = stripIndent`
98+
let res = [];
99+
let i = 0;
100+
for (i = 0; i < 5; i = i + 2) {
101+
res[i] = i;
102+
}
103+
`
104+
const context = mockContext(4, 'gpu')
105+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
106+
.transpiled
107+
108+
const cnt = transpiled.match(/__createKernel/g)?.length
109+
expect(cnt).toEqual(2)
110+
})
111+
112+
test('simple for loop with global variable update does not get transpiled', () => {
113+
const code = stripIndent`
114+
let res = [];
115+
let y = 5;
116+
for (let i = 0; i < 5; i = i + 1) {
117+
y = y + 1;
118+
res[i] = i;
119+
}
120+
`
121+
const context = mockContext(4, 'gpu')
122+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
123+
.transpiled
124+
125+
const cnt = transpiled.match(/__createKernel/g)?.length
126+
expect(cnt).toEqual(2)
127+
})
128+
129+
test('simple for loop with function call does not get transpiled', () => {
130+
const code = stripIndent`
131+
let res = [];
132+
let y = () => 1;
133+
for (let i = 0; i < 5; i = i + 1) {
134+
y();
135+
res[i] = i;
136+
}
137+
`
138+
const context = mockContext(4, 'gpu')
139+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
140+
.transpiled
141+
142+
const cnt = transpiled.match(/__createKernel/g)?.length
143+
expect(cnt).toEqual(2)
144+
})
145+
146+
test('simple for loop with double update does not get transpiled', () => {
147+
const code = stripIndent`
148+
let res = [];
149+
for (let i = 0; i < 5; i = i + 1) {
150+
res[i] = i;
151+
res[i] = i + 1;
152+
}
153+
`
154+
const context = mockContext(4, 'gpu')
155+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
156+
.transpiled
157+
158+
const cnt = transpiled.match(/__createKernel/g)?.length
159+
expect(cnt).toEqual(2)
160+
})
161+
162+
test('2 for loops with wrong indice order does not get transpiled', () => {
163+
const code = stripIndent`
164+
let res = [];
165+
for (let i = 0; i < 5; i = i + 1) {
166+
for (let j = 0; j < 5; j = j + 1) {
167+
res[j][i] = i + 1;
168+
}
169+
}
170+
`
171+
const context = mockContext(4, 'gpu')
172+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
173+
.transpiled
174+
175+
const cnt = transpiled.match(/__createKernel/g)?.length
176+
expect(cnt).toEqual(2)
177+
})
178+
179+
test('2 for loops with wrong indices order does not get transpiled', () => {
180+
const code = stripIndent`
181+
let res = [];
182+
for (let i = 0; i < 5; i = i + 1) {
183+
for (let j = 0; j < 5; j = j + 1) {
184+
res[j] = i + 1;
185+
}
186+
}
187+
`
188+
const context = mockContext(4, 'gpu')
189+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
190+
.transpiled
191+
192+
const cnt = transpiled.match(/__createKernel/g)?.length
193+
expect(cnt).toEqual(2)
194+
})
195+
196+
test('2 for loop case with 2 indices being written + use of result variable[i-1][j] does not get transpiled', () => {
197+
const code = stripIndent`
198+
let res = [];
199+
for (let i = 0; i < 5; i = i + 1) {
200+
res[i] = [];
201+
for (let j = 0; j < 5; j = j + 1) {
202+
res[i][j] = j;
203+
}
204+
}
205+
206+
for (let i = 0; i < 5; i = i + 1) {
207+
for (let j = 0; j < 5; j = j + 1) {
208+
let x = res[i-1][j];
209+
let y = math_abs(x * -5);
210+
res[i][j] = x + y;
211+
}
212+
}
213+
`
214+
const context = mockContext(4, 'gpu')
215+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
216+
.transpiled
217+
const cnt = transpiled.match(/__createKernel/g)?.length
218+
expect(cnt).toEqual(2)
219+
})
220+
221+
test('3 for loops with wrong indice order does not get transpiled', () => {
222+
const code = stripIndent`
223+
let res = [];
224+
for (let i = 0; i < 5; i = i + 1) {
225+
for (let j = 0; j < 5; j = j + 1) {
226+
for (let k = 0; k < 5; k = k + 1) {
227+
res[k][j][i] = i + 1;
228+
}
229+
}
230+
}
231+
`
232+
const context = mockContext(4, 'gpu')
233+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
234+
.transpiled
235+
236+
const cnt = transpiled.match(/__createKernel/g)?.length
237+
expect(cnt).toEqual(2)
238+
})
239+
240+
test('3 for loops with wrong indice order does not get transpiled', () => {
241+
const code = stripIndent`
242+
let res = [];
243+
for (let i = 0; i < 5; i = i + 1) {
244+
for (let j = 0; j < 5; j = j + 1) {
245+
for (let k = 0; k < 5; k = k + 1) {
246+
res[j][k] = i + 1;
247+
}
248+
}
249+
}
250+
`
251+
const context = mockContext(4, 'gpu')
252+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
253+
.transpiled
254+
255+
const cnt = transpiled.match(/__createKernel/g)?.length
256+
expect(cnt).toEqual(2)
257+
})

‎src/gpu/__tests__/runtimeError.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* tslint:disable:only-arrow-functions */
2+
import { __createKernel } from '../lib'
3+
import { TypeError } from '../../utils/rttc'
4+
5+
test('__createKernel with uninitialized array throws error', () => {
6+
const bounds = [5, 4]
7+
const extern = {}
8+
const f1 = function(this: any) {
9+
return this.thread.y * this.thread.x
10+
}
11+
12+
const arr: number[][] = []
13+
14+
const f2 = function(i: any, j: any) {
15+
return i * j
16+
}
17+
const f = () => __createKernel(bounds, extern, f1, arr, f2)
18+
expect(f).toThrow(TypeError)
19+
})
20+
21+
test('__createKernel with 2 loops + uninitialized array throws error', () => {
22+
const bounds = [5, 4, 3]
23+
const extern = {}
24+
const f1 = function(this: any) {
25+
return this.thread.z * this.thread.y * this.thread.x
26+
}
27+
28+
const arr: number[][][] = []
29+
for (let i = 0; i < 5; i = i + 1) {
30+
arr[i] = []
31+
}
32+
33+
const f2 = function(i: any, j: any, k: any) {
34+
return i * j * k
35+
}
36+
const f = () => __createKernel(bounds, extern, f1, arr, f2)
37+
expect(f).toThrow(TypeError)
38+
})

‎src/gpu/__tests__/runtimeOk.ts

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/* tslint:disable:only-arrow-functions */
2+
import { __createKernel } from '../lib'
3+
4+
test('__createKernel with 1 loop returns correct result', () => {
5+
const bounds = [5]
6+
const extern = {}
7+
const f1 = function() {
8+
return 1
9+
}
10+
const arr: number[] = []
11+
const f2 = function(i: any) {
12+
return 1
13+
}
14+
__createKernel(bounds, extern, f1, arr, f2)
15+
expect(arr).toEqual([1, 1, 1, 1, 1])
16+
})
17+
18+
test('__createKernel with 2 loops returns correct result', () => {
19+
const bounds = [5, 4]
20+
const extern = {}
21+
const f1 = function(this: any) {
22+
return this.thread.y * this.thread.x
23+
}
24+
25+
const arr: number[][] = []
26+
for (let i = 0; i < 5; i = i + 1) {
27+
arr[i] = []
28+
}
29+
30+
const f2 = function(i: any, j: any) {
31+
return i * j
32+
}
33+
__createKernel(bounds, extern, f1, arr, f2)
34+
expect(arr).toEqual([
35+
[0, 0, 0, 0],
36+
[0, 1, 2, 3],
37+
[0, 2, 4, 6],
38+
[0, 3, 6, 9],
39+
[0, 4, 8, 12]
40+
])
41+
})
42+
43+
test('__createKernel with 3 loop returns correct result', () => {
44+
const bounds = [5, 4, 3]
45+
const extern = {}
46+
const f1 = function(this: any) {
47+
return this.thread.z * this.thread.y * this.thread.x
48+
}
49+
50+
const arr: number[][][] = []
51+
for (let i = 0; i < 5; i = i + 1) {
52+
arr[i] = []
53+
for (let j = 0; j < 4; j = j + 1) {
54+
arr[i][j] = []
55+
}
56+
}
57+
58+
const f2 = function(i: any, j: any, k: any) {
59+
return i * j * k
60+
}
61+
__createKernel(bounds, extern, f1, arr, f2)
62+
expect(arr).toEqual([
63+
[
64+
[0, 0, 0],
65+
[0, 0, 0],
66+
[0, 0, 0],
67+
[0, 0, 0]
68+
],
69+
[
70+
[0, 0, 0],
71+
[0, 1, 2],
72+
[0, 2, 4],
73+
[0, 3, 6]
74+
],
75+
[
76+
[0, 0, 0],
77+
[0, 2, 4],
78+
[0, 4, 8],
79+
[0, 6, 12]
80+
],
81+
[
82+
[0, 0, 0],
83+
[0, 3, 6],
84+
[0, 6, 12],
85+
[0, 9, 18]
86+
],
87+
[
88+
[0, 0, 0],
89+
[0, 4, 8],
90+
[0, 8, 16],
91+
[0, 12, 24]
92+
]
93+
])
94+
})
95+
96+
test('__createKernel with 1 loop + return string returns correct result', () => {
97+
const bounds = [5]
98+
const extern = {}
99+
const f1 = function() {
100+
return 'a'
101+
}
102+
const arr: number[] = []
103+
const f2 = function() {
104+
return 'a'
105+
}
106+
__createKernel(bounds, extern, f1, arr, f2)
107+
expect(arr).toEqual(['a', 'a', 'a', 'a', 'a'])
108+
})
109+
110+
test('__createKernel with 1 loop + return number array returns correct result', () => {
111+
const bounds = [5]
112+
const extern = {}
113+
const f1 = function() {
114+
return [1, 2, 3]
115+
}
116+
const arr: number[] = []
117+
const f2 = function() {
118+
return [1, 2, 3]
119+
}
120+
__createKernel(bounds, extern, f1, arr, f2)
121+
expect(arr).toEqual([
122+
[1, 2, 3],
123+
[1, 2, 3],
124+
[1, 2, 3],
125+
[1, 2, 3],
126+
[1, 2, 3]
127+
])
128+
})
129+
130+
test('__createKernel with 1 loop + return string array returns correct result', () => {
131+
const bounds = [5]
132+
const extern = {}
133+
const f1 = function() {
134+
return ['a', 'a']
135+
}
136+
const arr: number[] = []
137+
const f2 = function() {
138+
return ['a', 'a']
139+
}
140+
__createKernel(bounds, extern, f1, arr, f2)
141+
expect(arr).toEqual([
142+
['a', 'a'],
143+
['a', 'a'],
144+
['a', 'a'],
145+
['a', 'a'],
146+
['a', 'a']
147+
])
148+
})
149+
150+
test('__createKernel with 1 loop + external variable returns correct result', () => {
151+
const bounds = [3]
152+
const extern = { y: 100 }
153+
const f1 = function(this: any) {
154+
return this.constants.y + this.thread.x
155+
}
156+
const arr: number[] = []
157+
const f2 = function() {
158+
return 1 + y
159+
}
160+
161+
const y = 100
162+
__createKernel(bounds, extern, f1, arr, f2)
163+
expect(arr).toEqual([101, 101, 101])
164+
})
165+
166+
test('__createKernel with 1 loop + external variable + math function returns correct result', () => {
167+
const bounds = [3]
168+
const extern = { y: 100 }
169+
const f1 = function(this: any) {
170+
return Math.abs(-this.constants.y + this.thread.x)
171+
}
172+
const arr: number[] = []
173+
174+
const y = 100
175+
176+
// tslint:disable-next-line
177+
const math_abs = Math.abs
178+
const f2 = function(i: any) {
179+
return math_abs(-y + i)
180+
}
181+
182+
__createKernel(bounds, extern, f1, arr, f2)
183+
expect(arr).toEqual([100, 99, 98])
184+
})

‎src/gpu/__tests__/transpile.ts

+320
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { mockContext } from '../../mocks/context'
2+
import { parse } from '../../parser/parser'
3+
import { stripIndent } from '../../utils/formatters'
4+
import { transpile } from '../../transpiler/transpiler'
5+
6+
test('simple for loop gets transpiled correctly', () => {
7+
const code = stripIndent`
8+
let res = [];
9+
for (let i = 0; i < 5; i = i + 1) {
10+
res[i] = i;
11+
}
12+
`
13+
const context = mockContext(4, 'gpu')
14+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
15+
.transpiled
16+
17+
const cnt = transpiled.match(/__createKernel/g)?.length
18+
expect(cnt).toEqual(3)
19+
})
20+
21+
test('many simple for loop gets transpiled correctly', () => {
22+
const code = stripIndent`
23+
let res = [];
24+
for (let i = 0; i < 5; i = i + 1) {
25+
res[i] = i;
26+
}
27+
28+
let res1 = [];
29+
for (let i = 0; i < 5; i = i + 1) {
30+
res1[i] = i;
31+
}
32+
`
33+
const context = mockContext(4, 'gpu')
34+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
35+
.transpiled
36+
37+
const cnt = transpiled.match(/__createKernel/g)?.length
38+
expect(cnt).toEqual(4)
39+
})
40+
41+
test('simple for loop with constant condition transpiled correctly', () => {
42+
const code = stripIndent`
43+
let res = [];
44+
const c = 10;
45+
for (let i = 0; i < c; i = i + 1) {
46+
res[i] = i;
47+
}
48+
`
49+
const context = mockContext(4, 'gpu')
50+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
51+
.transpiled
52+
const cnt = transpiled.match(/__createKernel/g)?.length
53+
expect(cnt).toEqual(3)
54+
})
55+
56+
test('simple for loop with let condition transpiled correctly', () => {
57+
const code = stripIndent`
58+
let res = [];
59+
let c = 10;
60+
for (let i = 0; i < c; i = i + 1) {
61+
res[i] = i;
62+
}
63+
`
64+
const context = mockContext(4, 'gpu')
65+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
66+
.transpiled
67+
68+
const cnt = transpiled.match(/__createKernel/g)?.length
69+
expect(cnt).toEqual(3)
70+
})
71+
72+
test('simple for loop with math function call transpiled correctly', () => {
73+
const code = stripIndent`
74+
let res = [];
75+
let c = 10;
76+
for (let i = 0; i < c; i = i + 1) {
77+
res[i] = math_abs(i);
78+
}
79+
`
80+
const context = mockContext(4, 'gpu')
81+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
82+
.transpiled
83+
84+
const cnt = transpiled.match(/__createKernel/g)?.length
85+
expect(cnt).toEqual(3)
86+
})
87+
88+
test('simple for loop with different end condition transpiled correctly', () => {
89+
const code = stripIndent`
90+
let res = [];
91+
const f = () => 5;
92+
let c = f();
93+
for (let i = 0; i < c; i = i + 1) {
94+
res[i] = i;
95+
}
96+
`
97+
const context = mockContext(4, 'gpu')
98+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
99+
.transpiled
100+
101+
const cnt = transpiled.match(/__createKernel/g)?.length
102+
expect(cnt).toEqual(3)
103+
})
104+
105+
test('2 for loop case gets transpiled correctly', () => {
106+
const code = stripIndent`
107+
let res = [];
108+
for (let i = 0; i < 5; i = i + 1) {
109+
for (let j = 0; j < 5; j = j + 1) {
110+
res[i] = i;
111+
}
112+
}
113+
`
114+
const context = mockContext(4, 'gpu')
115+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
116+
.transpiled
117+
118+
const cnt = transpiled.match(/__createKernel/g)?.length
119+
expect(cnt).toEqual(3)
120+
})
121+
122+
test('2 for loop case with body gets transpiled correctly', () => {
123+
const code = stripIndent`
124+
let res = [];
125+
for (let i = 0; i < 5; i = i + 1) {
126+
let sum = 0;
127+
for (let j = 0; j < 5; j = j + 1) {
128+
sum = sum + j;
129+
}
130+
res[i] = sum;
131+
}
132+
`
133+
const context = mockContext(4, 'gpu')
134+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
135+
.transpiled
136+
137+
const cnt = transpiled.match(/__createKernel/g)?.length
138+
expect(cnt).toEqual(3)
139+
})
140+
141+
test('2 for loop case with 2 indices being written to gets transpiled correctly', () => {
142+
const code = stripIndent`
143+
let res = [];
144+
for (let i = 0; i < 5; i = i + 1) {
145+
for (let j = 0; j < 5; j = j + 1) {
146+
res[i][j] = i*j;
147+
}
148+
}
149+
`
150+
const context = mockContext(4, 'gpu')
151+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
152+
.transpiled
153+
154+
const cnt = transpiled.match(/__createKernel/g)?.length
155+
expect(cnt).toEqual(3)
156+
})
157+
158+
test('2 for loop case with 2 indices being written + local updates to gets transpiled correctly', () => {
159+
const code = stripIndent`
160+
let res1 = [];
161+
for (let i = 0; i < 5; i = i + 1) {
162+
res1[i] = [];
163+
for (let j = 0; j < 5; j = j + 1) {
164+
res1[i][j] = j;
165+
}
166+
}
167+
168+
let res = [];
169+
for (let i = 0; i < 5; i = i + 1) {
170+
for (let j = 0; j < 5; j = j + 1) {
171+
let x = res1[i][j];
172+
let y = math_abs(x * -5);
173+
res[i][j] = x + y;
174+
}
175+
}
176+
`
177+
const context = mockContext(4, 'gpu')
178+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
179+
.transpiled
180+
181+
const cnt = transpiled.match(/__createKernel/g)?.length
182+
expect(cnt).toEqual(3)
183+
})
184+
185+
test('2 for loop case with 2 indices being written + use of result variable[i][j] gets transpiled', () => {
186+
const code = stripIndent`
187+
let res = [];
188+
for (let i = 0; i < 5; i = i + 1) {
189+
res[i] = [];
190+
for (let j = 0; j < 5; j = j + 1) {
191+
res[i][j] = j;
192+
}
193+
}
194+
195+
for (let i = 0; i < 5; i = i + 1) {
196+
for (let j = 0; j < 5; j = j + 1) {
197+
let x = res[i][j];
198+
let y = math_abs(x * -5);
199+
res[i][j] = x + y;
200+
}
201+
}
202+
`
203+
const context = mockContext(4, 'gpu')
204+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
205+
.transpiled
206+
const cnt = transpiled.match(/__createKernel/g)?.length
207+
expect(cnt).toEqual(3)
208+
})
209+
210+
test('3 for loop case with 1 index being written to gets transpiled correctly', () => {
211+
const code = stripIndent`
212+
let res = [];
213+
for (let i = 0; i < 5; i = i + 1) {
214+
for (let j = 0; j < 5; j = j + 1) {
215+
for (let k = 0; k < 5; k = k + 1) {
216+
res[i] = i*j;
217+
}
218+
}
219+
}
220+
`
221+
const context = mockContext(4, 'gpu')
222+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
223+
.transpiled
224+
225+
const cnt = transpiled.match(/__createKernel/g)?.length
226+
expect(cnt).toEqual(3)
227+
})
228+
229+
test('3 for loop case with 2 indices being written to gets transpiled correctly', () => {
230+
const code = stripIndent`
231+
let res = [];
232+
for (let i = 0; i < 5; i = i + 1) {
233+
for (let j = 0; j < 5; j = j + 1) {
234+
for (let k = 0; k < 5; k = k + 1) {
235+
res[i][j] = i*j;
236+
}
237+
}
238+
}
239+
`
240+
const context = mockContext(4, 'gpu')
241+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
242+
.transpiled
243+
244+
const cnt = transpiled.match(/__createKernel/g)?.length
245+
expect(cnt).toEqual(3)
246+
})
247+
248+
test('3 for loop case with 3 indices being written to gets transpiled correctly', () => {
249+
const code = stripIndent`
250+
let res = [];
251+
for (let i = 0; i < 5; i = i + 1) {
252+
for (let j = 0; j < 5; j = j + 1) {
253+
for (let k = 0; k < 5; k = k + 1) {
254+
res[i][j][k] = i*j;
255+
}
256+
}
257+
}
258+
`
259+
const context = mockContext(4, 'gpu')
260+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
261+
.transpiled
262+
263+
const cnt = transpiled.match(/__createKernel/g)?.length
264+
expect(cnt).toEqual(3)
265+
})
266+
267+
test('many for loop case - matrix multiplication (2 transpilations)', () => {
268+
const code = stripIndent`
269+
const size = 10;
270+
const L = [];
271+
const R = [];
272+
for (let r = 0; r < size; r = r + 1) {
273+
L[r] = [];
274+
R[r] = [];
275+
for (let c = 0; c < size; c = c + 1) {
276+
L[r][c] = r*c;
277+
R[r][c] = r + c;
278+
}
279+
}
280+
281+
const res = [];
282+
for (let r = 0; r < size; r = r + 1) {
283+
res[r] = [];
284+
}
285+
286+
for (let r = 0; r < size; r = r + 1) {
287+
for (let c = 0; c < size; c = c + 1) {
288+
let sum = 0;
289+
for (let i = 0; i < size; i = i + 1) {
290+
sum = sum + L[r][i] * R[i][c];
291+
}
292+
res[r][c] = sum;
293+
}
294+
}
295+
`
296+
const context = mockContext(4, 'gpu')
297+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
298+
.transpiled
299+
300+
const cnt = transpiled.match(/__createKernel/g)?.length
301+
expect(cnt).toEqual(4)
302+
})
303+
304+
test('resolve naming conflicts if __createKernel is used', () => {
305+
const code = stripIndent`
306+
const __createKernel = 10;
307+
308+
let res = [];
309+
for (let i = 0; i < 5; i = i + 1) {
310+
res[i] = i;
311+
}
312+
`
313+
const context = mockContext(4, 'gpu')
314+
const transpiled = transpile(parse(code, context)!, context.contextId, false, context.variant)
315+
.transpiled
316+
317+
// a new kernel function with name __createKernel0 should be created here
318+
const cntNewName = transpiled.match(/__createKernel0/g)?.length
319+
expect(cntNewName).toEqual(2)
320+
})

‎src/gpu/gpu.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import GPUTransformer from './transfomer'
2+
import { AllowedDeclarations } from '../types'
3+
import * as create from '../utils/astCreator'
4+
import * as es from 'estree'
5+
6+
// top-level gpu functions that call our code
7+
8+
// transpiles if possible and returns display statements to end user
9+
export function transpileToGPU(program: es.Program, kernelFunction: string): es.Statement[] {
10+
// create unique kernel name
11+
GPUTransformer.globalIds.__createKernel = create.identifier(kernelFunction)
12+
13+
const transformer = new GPUTransformer(program)
14+
const res = transformer.transform()
15+
16+
const gpuDisplayStatements = []
17+
// add some display statements to program
18+
if (res.length > 0) {
19+
for (const arr of res) {
20+
let debug = `Attempting to optimize ${arr[1]} levels of nested loops starting on line ${arr[0]}`
21+
if (arr[1] === 1) {
22+
debug = `Attempting to optimize the loop on line ${arr[0]}`
23+
}
24+
gpuDisplayStatements.push(
25+
create.expressionStatement(
26+
create.callExpression(create.identifier('display'), [create.literal(debug)])
27+
)
28+
)
29+
}
30+
}
31+
return gpuDisplayStatements
32+
}
33+
34+
export function getInternalNamesForGPU(): Set<string> {
35+
return new Set(Object.entries(GPUTransformer.globalIds).map(([key, { name }]) => key))
36+
}
37+
38+
export function getInternalFunctionsForGPU(info: any, cid: any) {
39+
return Object.entries(GPUTransformer.globalIds).map(([key, { name }]) => {
40+
const kind: AllowedDeclarations = 'const'
41+
const value: es.Expression = create.callExpression(
42+
create.memberExpression(
43+
create.memberExpression(
44+
{
45+
type: 'MemberExpression',
46+
object: info.native,
47+
property: create.literal(cid),
48+
computed: true
49+
},
50+
'gpu'
51+
),
52+
'get'
53+
),
54+
[create.literal(key)]
55+
)
56+
return create.declaration(name, kind, value)
57+
})
58+
}

‎src/gpu/lib.ts

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { GPU } from 'gpu.js'
2+
import { TypeError } from '../utils/rttc'
3+
import { isArray } from 'util'
4+
5+
// Heuristic : Only use GPU if array is bigger than this
6+
const MAX_SIZE = 200
7+
8+
// helper function to build 2D array output
9+
function buildArray(arr: Float32Array[][], end: any, res: any) {
10+
for (let i = 0; i < end[0]; i++) {
11+
res[i] = prettyOutput(arr[i])
12+
}
13+
}
14+
15+
function build2DArray(arr: Float32Array[][], end: any, res: any) {
16+
for (let i = 0; i < end[0]; i++) {
17+
for (let j = 0; j < end[1]; j++) {
18+
res[i][j] = prettyOutput(arr[i][j])
19+
}
20+
}
21+
}
22+
23+
// helper function to build 3D array output
24+
function build3DArray(arr: Float32Array[][][], end: any, res: any) {
25+
for (let i = 0; i < end[0]; i++) {
26+
for (let j = 0; j < end[1]; j++) {
27+
for (let k = 0; k < end[2]; k++) {
28+
res[i][j][k] = prettyOutput(arr[i][j][k])
29+
}
30+
}
31+
}
32+
}
33+
34+
function prettyOutput(arr: any): any {
35+
if (!(arr instanceof Float32Array)) {
36+
return arr
37+
}
38+
39+
const res = arr.map(x => prettyOutput(x))
40+
return Array.from(res)
41+
}
42+
43+
// helper function to check array is initialized
44+
function checkArray(arr: any): boolean {
45+
return Array.isArray(arr)
46+
}
47+
48+
// helper function to check 2D array is initialized
49+
function checkArray2D(arr: any, end: any): boolean {
50+
for (let i = 0; i < end[0]; i = i + 1) {
51+
if (!Array.isArray(arr[i])) return false
52+
}
53+
return true
54+
}
55+
56+
// helper function to check 3D array is initialized
57+
function checkArray3D(arr: any, end: any): boolean {
58+
for (let i = 0; i < end[0]; i = i + 1) {
59+
if (!Array.isArray(arr[i])) return false
60+
for (let j = 0; j < end[1]; j = j + 1) {
61+
if (!Array.isArray(arr[i][j])) return false
62+
}
63+
}
64+
return true
65+
}
66+
67+
function checkForNumber(arr: any): boolean {
68+
if (isArray(arr)) {
69+
return arr.length > 0 && arr.filter(x => !checkForNumber(x)).length === 0
70+
}
71+
72+
return typeof arr === 'number'
73+
}
74+
75+
/*
76+
* we only use the gpu if:
77+
* 1. we are working with numbers
78+
* 2. we have a large array (> 100 elements)
79+
*/
80+
function checkValidGPU(f: any, end: any): boolean {
81+
let res: any
82+
if (end.length === 1) res = f(0)
83+
if (end.length === 2) res = f(0, 0)
84+
if (end.length === 3) res = f(0, 0, 0)
85+
86+
if (typeof res !== 'number') {
87+
if (!Array.isArray(res)) {
88+
return false
89+
}
90+
91+
if (!checkForNumber(res)) {
92+
return false
93+
}
94+
}
95+
96+
let cnt = 1
97+
for (const i of end) {
98+
cnt = cnt * i
99+
}
100+
101+
return cnt > MAX_SIZE
102+
}
103+
104+
// just run on js!
105+
function manualRun(f: any, end: any, res: any) {
106+
function build() {
107+
for (let i = 0; i < end[0]; i++) {
108+
res[i] = f(i)
109+
}
110+
return
111+
}
112+
113+
function build2D() {
114+
for (let i = 0; i < end[0]; i = i + 1) {
115+
for (let j = 0; j < end[1]; j = j + 1) {
116+
res[i][j] = f(i, j)
117+
}
118+
}
119+
return
120+
}
121+
122+
function build3D() {
123+
for (let i = 0; i < end[0]; i = i + 1) {
124+
for (let j = 0; j < end[1]; j = j + 1) {
125+
for (let k = 0; k < end[2]; k = k + 1) {
126+
res[i][j][k] = f(i, j, k)
127+
}
128+
}
129+
}
130+
return
131+
}
132+
133+
if (end.length === 1) return build()
134+
if (end.length === 2) return build2D()
135+
return build3D()
136+
}
137+
138+
/* main function that runs code on the GPU (using gpu.js library)
139+
* @end : end bounds for array
140+
* @extern : external variable definitions {}
141+
* @f : function run as on GPU threads
142+
* @arr : array to be written to
143+
*/
144+
export function __createKernel(end: any, extern: any, f: any, arr: any, f2: any) {
145+
const gpu = new GPU()
146+
const nend = []
147+
for (let i = end.length - 1; i >= 0; i--) {
148+
nend.push(end[i])
149+
}
150+
151+
// check array is initialized properly
152+
let ok = checkArray(arr)
153+
let err = ''
154+
if (!ok) {
155+
err = typeof arr
156+
}
157+
158+
// TODO: find a cleaner way to do this
159+
if (end.length > 1) {
160+
ok = ok && checkArray2D(arr, end)
161+
if (!ok) {
162+
err = 'undefined'
163+
}
164+
}
165+
166+
if (end.length > 2) {
167+
ok = ok && checkArray3D(arr, end)
168+
if (!ok) {
169+
err = 'undefined'
170+
}
171+
}
172+
173+
if (!ok) {
174+
throw new TypeError(arr, '', 'object or array', err)
175+
}
176+
177+
// check if program is valid to run on GPU
178+
ok = checkValidGPU(f2, end)
179+
if (!ok) {
180+
manualRun(f2, end, arr)
181+
return
182+
}
183+
184+
// external variables to be in the GPU
185+
const out = { constants: {} }
186+
out.constants = extern
187+
188+
const gpuFunction = gpu.createKernel(f, out).setOutput(nend)
189+
const res = gpuFunction() as any
190+
if (end.length === 1) buildArray(res, end, arr)
191+
if (end.length === 2) build2DArray(res, end, arr)
192+
if (end.length === 3) build3DArray(res, end, arr)
193+
}

‎src/gpu/transfomer.ts

+320
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import * as es from 'estree'
2+
import { ancestor, simple, make } from 'acorn-walk/dist/walk'
3+
import * as create from '../utils/astCreator'
4+
import GPULoopVerifier from './verification/loopVerifier'
5+
import GPUBodyVerifier from './verification/bodyVerifier'
6+
7+
/*
8+
* GPU Transformer runs through the program and transpiles for loops to GPU code
9+
* Upon termination, the AST would be mutated accordingly
10+
* e.g.
11+
* let res = 0;
12+
* for (let i = 0; i < 5; i = i + 1) {
13+
* res[i] = 5;
14+
* }
15+
* would become:
16+
* let res = 0;
17+
* __createKernel(....)
18+
*/
19+
class GPUTransformer {
20+
// program to mutate
21+
program: es.Program
22+
23+
// helps reference the main function
24+
static globalIds = {
25+
__createKernel: create.identifier('__createKernel')
26+
}
27+
28+
outputArray: es.Identifier
29+
innerBody: any
30+
counters: string[]
31+
end: es.Expression[]
32+
state: number
33+
localVar: Set<string>
34+
outerVariables: any
35+
targetBody: any
36+
37+
constructor(program: es.Program) {
38+
this.program = program
39+
}
40+
41+
// transforms away top-level for loops if possible
42+
transform = (): number[][] => {
43+
const gpuTranspile = this.gpuTranspile
44+
const res: number[][] = []
45+
46+
// tslint:disable
47+
simple(
48+
this.program,
49+
{
50+
ForStatement(node: es.ForStatement) {
51+
let state = gpuTranspile(node)
52+
if (state > 0 && node.loc) {
53+
res.push([node.loc.start.line, state])
54+
}
55+
}
56+
},
57+
make({ ForStatement: () => {} })
58+
)
59+
// tslint:enable
60+
61+
return res
62+
}
63+
64+
/*
65+
* Here we transpile away a for loop:
66+
* 1. Check if it meets our specifications
67+
* 2. Get external variables + target body (body to be run across gpu threads)
68+
* 3. Build a AST Node for (2) - this will be given to (8)
69+
* 4. Change assignment in body to a return statement
70+
* 5. In body, update all math_* calls to become Math.* calls
71+
* 6. In body, update all external variable references
72+
* 7. In body, update reference to counters
73+
* 8. Call __createKernel and assign it to our external variable
74+
*/
75+
gpuTranspile = (node: es.ForStatement): number => {
76+
// initialize our class variables
77+
this.state = 0
78+
this.counters = []
79+
this.end = []
80+
81+
// 1. verification of outer loops + body
82+
this.checkOuterLoops(node)
83+
// no gpu loops found
84+
if (this.counters.length === 0 || new Set(this.counters).size !== this.counters.length) {
85+
return 0
86+
}
87+
88+
const verifier = new GPUBodyVerifier(this.program, this.innerBody, this.counters)
89+
if (verifier.state === 0) {
90+
return 0
91+
}
92+
93+
this.state = verifier.state
94+
this.outputArray = verifier.outputArray
95+
this.localVar = verifier.localVar
96+
97+
// 2. get external variables + the main body
98+
this.getOuterVariables()
99+
this.getTargetBody(node)
100+
101+
// 3. Build a AST Node of all outer variables
102+
const externObject: es.Property[] = []
103+
for (const key in this.outerVariables) {
104+
if (this.outerVariables.hasOwnProperty(key)) {
105+
const val = this.outerVariables[key]
106+
107+
// push in a deep copy of the identifier
108+
// this is needed cos we modify it later
109+
externObject.push(create.property(key, JSON.parse(JSON.stringify(val))))
110+
}
111+
}
112+
113+
// 4. Change assignment in body to a return statement
114+
const checker = verifier.getArrayName
115+
const locals = this.localVar
116+
ancestor(this.targetBody, {
117+
AssignmentExpression(nx: es.AssignmentExpression, ancstor: es.Node[]) {
118+
// assigning to local val, it's okay
119+
if (nx.left.type === 'Identifier') {
120+
return
121+
}
122+
123+
if (nx.left.type !== 'MemberExpression') {
124+
return
125+
}
126+
127+
const id = checker(nx.left)
128+
if (locals.has(id.name)) {
129+
return
130+
}
131+
132+
const sz = ancstor.length
133+
create.mutateToReturnStatement(ancstor[sz - 2], nx.right)
134+
}
135+
})
136+
137+
// deep copy here (for runtime checks)
138+
const params: es.Identifier[] = []
139+
for (let i = 0; i < this.state; i++) {
140+
params.push(create.identifier(this.counters[i]))
141+
}
142+
const tempNode = create.functionExpression(params, JSON.parse(JSON.stringify(this.targetBody)))
143+
144+
// 5. Update all math_* calls to become Math.*
145+
simple(this.targetBody, {
146+
CallExpression(nx: es.CallExpression) {
147+
if (nx.callee.type !== 'Identifier') {
148+
return
149+
}
150+
151+
const functionName = nx.callee.name
152+
const term = functionName.split('_')[1]
153+
const args: es.Expression[] = nx.arguments as any
154+
155+
create.mutateToCallExpression(
156+
nx,
157+
create.memberExpression(create.identifier('Math'), term),
158+
args
159+
)
160+
}
161+
})
162+
163+
// 6. Update all external variable references in body
164+
// e.g. let res = 1 + y; where y is an external variable
165+
// becomes let res = 1 + this.constants.y;
166+
167+
const names = [this.outputArray.name, ...this.counters, 'Math']
168+
simple(this.targetBody, {
169+
Identifier(nx: es.Identifier) {
170+
// ignore these names
171+
if (names.includes(nx.name) || locals.has(nx.name)) {
172+
return
173+
}
174+
175+
create.mutateToMemberExpression(
176+
nx,
177+
create.memberExpression(create.identifier('this'), 'constants'),
178+
create.identifier(nx.name)
179+
)
180+
}
181+
})
182+
183+
// 7. Update reference to counters
184+
// e.g. let res = 1 + i; where i is a counter
185+
// becomes let res = 1 + this.thread.x;
186+
187+
// depending on state the mappings will change
188+
let threads = ['x']
189+
if (this.state === 2) threads = ['y', 'x']
190+
if (this.state === 3) threads = ['z', 'y', 'x']
191+
192+
const counters: string[] = []
193+
for (let i = 0; i < this.state; i = i + 1) {
194+
counters.push(this.counters[i])
195+
}
196+
197+
simple(this.targetBody, {
198+
Identifier(nx: es.Identifier) {
199+
let x = -1
200+
for (let i = 0; i < counters.length; i = i + 1) {
201+
if (nx.name === counters[i]) {
202+
x = i
203+
break
204+
}
205+
}
206+
207+
if (x === -1) {
208+
return
209+
}
210+
211+
const id = threads[x]
212+
create.mutateToMemberExpression(
213+
nx,
214+
create.memberExpression(create.identifier('this'), 'thread'),
215+
create.identifier(id)
216+
)
217+
}
218+
})
219+
220+
// 8. we transpile the loop to a function call, __createKernel
221+
const kernelFunction = create.functionExpression([], this.targetBody)
222+
create.mutateToExpressionStatement(
223+
node,
224+
create.callExpression(
225+
GPUTransformer.globalIds.__createKernel,
226+
[
227+
create.arrayExpression(this.end),
228+
create.objectExpression(externObject),
229+
kernelFunction,
230+
this.outputArray,
231+
tempNode
232+
],
233+
node.loc!
234+
)
235+
)
236+
237+
return this.state
238+
}
239+
240+
// verification of outer loops using our verifier
241+
checkOuterLoops = (node: es.ForStatement) => {
242+
let currForLoop = node
243+
while (currForLoop.type === 'ForStatement') {
244+
const detector = new GPULoopVerifier(currForLoop)
245+
if (!detector.ok) {
246+
break
247+
}
248+
249+
this.innerBody = currForLoop.body
250+
this.counters.push(detector.counter)
251+
this.end.push(detector.end)
252+
253+
if (this.innerBody.type !== 'BlockStatement') {
254+
break
255+
}
256+
257+
if (this.innerBody.body.length > 1 || this.innerBody.body.length === 0) {
258+
break
259+
}
260+
261+
currForLoop = this.innerBody.body[0]
262+
}
263+
}
264+
265+
/*
266+
* Based on state, gets the correct body to be run across threads
267+
* e.g. state = 2 (2 top level loops skipped)
268+
* for (...) {
269+
* for (...) {
270+
* let x = 1;
271+
* res[i] = x + 1
272+
* }
273+
* }
274+
*
275+
* returns:
276+
*
277+
* {
278+
* let x = 1;
279+
* res[i] = x + 1
280+
* }
281+
*/
282+
getTargetBody(node: es.ForStatement) {
283+
let mv = this.state
284+
this.targetBody = node
285+
while (mv > 1) {
286+
this.targetBody = this.targetBody.body.body[0]
287+
mv--
288+
}
289+
this.targetBody = this.targetBody.body
290+
}
291+
292+
// get all variables defined outside the block (on right hand side)
293+
// TODO: method can be more optimized
294+
getOuterVariables() {
295+
// set some local variables for walking
296+
const curr = this.innerBody
297+
const localVar = this.localVar
298+
const counters = this.counters
299+
const output = this.outputArray.name
300+
301+
const varDefinitions = {}
302+
simple(curr, {
303+
Identifier(node: es.Identifier) {
304+
if (
305+
localVar.has(node.name) ||
306+
counters.includes(node.name) ||
307+
node.name === output ||
308+
node.name.startsWith('math_')
309+
) {
310+
return
311+
}
312+
313+
varDefinitions[node.name] = node
314+
}
315+
})
316+
this.outerVariables = varDefinitions
317+
}
318+
}
319+
320+
export default GPUTransformer

‎src/gpu/verification/bodyVerifier.ts

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import * as es from 'estree'
2+
import { simple, make } from 'acorn-walk/dist/walk'
3+
4+
/*
5+
* GPU Body verifier helps to ensure the body is parallelizable
6+
* It does a series of checks to make sure the loop can be parallelized easily
7+
* Upon termination will update:
8+
* @state: number that indicates the dimensions you can parallize till (max 3)
9+
* @localVar: local variables in the body
10+
* @outputArray: array that is being written to
11+
*/
12+
class GPUBodyVerifier {
13+
program: es.Program
14+
node: es.Statement
15+
16+
state: number
17+
localVar: Set<string>
18+
counters: string[]
19+
outputArray: es.Identifier
20+
21+
/**
22+
*
23+
* @param node body to be verified
24+
* @param counters list of for loop counters (to check array assignment)
25+
*/
26+
constructor(program: es.Program, node: es.Statement, counters: string[]) {
27+
this.program = program
28+
this.node = node
29+
this.counters = counters
30+
this.state = 0
31+
this.checkBody(node)
32+
}
33+
34+
/*
35+
* Checks if the GPU body is valid
36+
* 1. No return/function declarations/break/continue
37+
* 2. No functions except math_*
38+
* 3. Only ONE assignment to a global result variable
39+
* 4. Assigning to an array at specific indices (i, j, k from for loop counters)
40+
*/
41+
checkBody = (node: es.Statement) => {
42+
let ok: boolean = true
43+
44+
// 1. check illegal statements
45+
simple(node, {
46+
FunctionDeclaration() {
47+
ok = false
48+
},
49+
ArrowFunctionExpression() {
50+
ok = false
51+
},
52+
ReturnStatement() {
53+
ok = false
54+
},
55+
BreakStatement() {
56+
ok = false
57+
},
58+
ContinueStatement() {
59+
ok = false
60+
}
61+
})
62+
63+
if (!ok) {
64+
return
65+
}
66+
67+
// 2. check function calls are only to math_*
68+
const mathFuncCheck = new RegExp(/^math_[a-z]+$/)
69+
simple(node, {
70+
CallExpression(nx: es.CallExpression) {
71+
if (nx.callee.type !== 'Identifier') {
72+
ok = false
73+
return
74+
}
75+
76+
const functionName = nx.callee.name
77+
if (!mathFuncCheck.test(functionName)) {
78+
ok = false
79+
return
80+
}
81+
}
82+
})
83+
84+
if (!ok) {
85+
return
86+
}
87+
88+
// 3. check there is only ONE assignment to a global result variable
89+
90+
// get all local variables
91+
const localVar = new Set<string>()
92+
simple(node, {
93+
VariableDeclaration(nx: es.VariableDeclaration) {
94+
if (nx.declarations[0].id.type === 'Identifier') {
95+
localVar.add(nx.declarations[0].id.name)
96+
}
97+
}
98+
})
99+
this.localVar = localVar
100+
101+
// make sure only one assignment
102+
const resultExpr: es.AssignmentExpression[] = []
103+
const checker = this.getArrayName
104+
simple(node, {
105+
AssignmentExpression(nx: es.AssignmentExpression) {
106+
// assigning to local val, it's okay
107+
if (nx.left.type === 'Identifier' && localVar.has(nx.left.name)) {
108+
return
109+
}
110+
111+
if (nx.left.type === 'MemberExpression') {
112+
const chk = checker(nx.left)
113+
if (localVar.has(chk.name)) {
114+
return
115+
}
116+
}
117+
118+
resultExpr.push(nx)
119+
}
120+
})
121+
122+
// too many assignments!
123+
if (resultExpr.length !== 1) {
124+
return
125+
}
126+
127+
// 4. check assigning to array at specific indices
128+
129+
// not assigning to array
130+
if (resultExpr[0].left.type !== 'MemberExpression') {
131+
return
132+
}
133+
134+
// check res assignment and its counters
135+
const res = this.getPropertyAccess(resultExpr[0].left)
136+
if (res.length === 0 || res.length > this.counters.length) {
137+
return
138+
}
139+
140+
// check result variable is not used anywhere with wrong indices
141+
const getProp = this.getPropertyAccess
142+
const resArr = this.outputArray
143+
simple(
144+
node,
145+
{
146+
MemberExpression(nx: es.MemberExpression) {
147+
const chk = checker(nx)
148+
if (chk.name !== resArr.name) {
149+
return
150+
}
151+
152+
// get indices
153+
const indices = getProp(nx)
154+
if (JSON.stringify(indices) === JSON.stringify(res)) {
155+
return
156+
}
157+
158+
ok = false
159+
}
160+
},
161+
// tslint:disable-next-line
162+
make({ MemberExpression: () => {} })
163+
)
164+
165+
if (!ok) {
166+
return
167+
}
168+
169+
for (let i = 0; i < this.counters.length; i++) {
170+
if (res[i] !== this.counters[i]) break
171+
this.state++
172+
}
173+
174+
// we only can have upto 3 states
175+
if (this.state > 3) this.state = 3
176+
}
177+
178+
getArrayName = (node: es.MemberExpression): es.Identifier => {
179+
let curr: any = node
180+
while (curr.type === 'MemberExpression') {
181+
curr = curr.object
182+
}
183+
return curr
184+
}
185+
186+
// helper function that helps to get indices accessed from array
187+
// e.g. returns i, j for res[i][j]
188+
getPropertyAccess = (node: es.MemberExpression): string[] => {
189+
const res: string[] = []
190+
let ok: boolean = true
191+
let curr: any = node
192+
while (curr.type === 'MemberExpression') {
193+
if (curr.property.type !== 'Identifier') {
194+
ok = false
195+
break
196+
}
197+
198+
res.push(curr.property.name)
199+
curr = curr.object
200+
}
201+
202+
if (!ok) {
203+
return []
204+
}
205+
206+
this.outputArray = curr
207+
return res.reverse()
208+
}
209+
}
210+
211+
export default GPUBodyVerifier

‎src/gpu/verification/loopVerifier.ts

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as es from 'estree'
2+
3+
/*
4+
* Loop Detector helps to verify if a for loop can be parallelized with a GPU
5+
* Updates ok, counter and end upon termination
6+
* @ok: false if not valid, true of valid
7+
* @end: if valid, stores the end of the loop
8+
* @counter: stores the string representation of the counter
9+
*/
10+
class GPULoopVerifier {
11+
// for loop that we are looking at
12+
node: es.ForStatement
13+
14+
counter: string
15+
end: es.Expression
16+
ok: boolean
17+
18+
constructor(node: es.ForStatement) {
19+
this.node = node
20+
this.forLoopTransform(this.node)
21+
}
22+
23+
forLoopTransform = (node: es.ForStatement) => {
24+
if (!node.init || !node.update || !node.test) {
25+
return
26+
}
27+
28+
this.ok =
29+
this.hasCounter(node.init) && this.hasCondition(node.test) && this.hasUpdate(node.update)
30+
}
31+
32+
/*
33+
* Checks if the loop counter is valid
34+
* it has to be "let <identifier> = 0;"
35+
*/
36+
hasCounter = (node: es.VariableDeclaration | es.Expression | null): boolean => {
37+
if (!node || node.type !== 'VariableDeclaration') {
38+
return false
39+
}
40+
41+
if (node.kind !== 'let') {
42+
return false
43+
}
44+
45+
const declaration: es.VariableDeclarator[] = node.declarations
46+
if (declaration.length > 1) {
47+
return false
48+
}
49+
50+
const initializer: es.VariableDeclarator = declaration[0]
51+
if (initializer.id.type !== 'Identifier' || !initializer.init) {
52+
return false
53+
}
54+
55+
this.counter = initializer.id.name
56+
57+
const set: es.Expression = initializer.init
58+
if (!set || set.type !== 'Literal' || set.value !== 0) {
59+
return false
60+
}
61+
62+
return true
63+
}
64+
65+
/*
66+
* Checks if the loop condition is valid
67+
* it has to be "<identifier> < <number>;"
68+
* identifier is the same as the one initialized above
69+
*/
70+
hasCondition = (node: es.Expression): boolean => {
71+
if (node.type !== 'BinaryExpression') {
72+
return false
73+
}
74+
75+
if (!(node.operator === '<' || node.operator === '<=')) {
76+
return false
77+
}
78+
79+
const lv: es.Expression = node.left
80+
if (lv.type !== 'Identifier' || lv.name !== this.counter) {
81+
return false
82+
}
83+
84+
const rv = node.right
85+
if (!(rv.type === 'Identifier' || rv.type === 'Literal')) {
86+
return false
87+
}
88+
89+
this.end = rv
90+
return true
91+
}
92+
93+
/*
94+
* Checks if the loop update is valid
95+
* it has to be "<identifier> = <identifier> + 1;"
96+
* identifier is the same as the one initialized above
97+
*/
98+
hasUpdate = (node: es.Expression): boolean => {
99+
if (node.type !== 'AssignmentExpression') {
100+
return false
101+
}
102+
103+
if (node.operator !== '=') {
104+
return false
105+
}
106+
107+
if (node.left.type !== 'Identifier' || node.left.name !== this.counter) {
108+
return false
109+
}
110+
111+
if (node.right.type !== 'BinaryExpression') {
112+
return false
113+
}
114+
115+
const rv = node.right
116+
if (rv.operator !== '+') {
117+
return false
118+
}
119+
120+
const identifierLeft = rv.left.type === 'Identifier' && rv.left.name === this.counter
121+
const identifierRight = rv.right.type === 'Identifier' && rv.right.name === this.counter
122+
123+
const literalLeft = rv.left.type === 'Literal' && rv.left.value === 1
124+
const literalRight = rv.right.type === 'Literal' && rv.right.value === 1
125+
126+
// we allow both i = i + 1 and i = 1 + i
127+
return (identifierLeft && literalRight) || (identifierRight && literalLeft)
128+
}
129+
}
130+
131+
export default GPULoopVerifier

‎src/transpiler/transpiler.ts

+185-109
Large diffs are not rendered by default.

‎src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface Comment {
5757
}
5858

5959
export type ExecutionMethod = 'native' | 'interpreter' | 'auto'
60-
export type Variant = 'wasm' | 'lazy' | 'non-det' | 'concurrent' | 'default' // this might replace EvaluationMethod
60+
export type Variant = 'wasm' | 'lazy' | 'non-det' | 'concurrent' | 'gpu' | 'default' // this might replace EvaluationMethod
6161

6262
export interface Context<T = any> {
6363
/** The source version used */

‎src/utils/astCreator.ts

+36
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,42 @@ export const mutateToCallExpression = (
138138
node.arguments = args
139139
}
140140

141+
export const mutateToAssignmentExpression = (
142+
node: es.Node,
143+
left: es.Pattern,
144+
right: es.Expression
145+
) => {
146+
node.type = 'AssignmentExpression'
147+
node = node as es.AssignmentExpression
148+
node.operator = '='
149+
node.left = left
150+
node.right = right
151+
}
152+
153+
export const mutateToExpressionStatement = (node: es.Node, expr: es.Expression) => {
154+
node.type = 'ExpressionStatement'
155+
node = node as es.ExpressionStatement
156+
node.expression = expr
157+
}
158+
159+
export const mutateToReturnStatement = (node: es.Node, expr: es.Expression) => {
160+
node.type = 'ReturnStatement'
161+
node = node as es.ReturnStatement
162+
node.argument = expr
163+
}
164+
165+
export const mutateToMemberExpression = (
166+
node: es.Node,
167+
obj: es.Expression,
168+
prop: es.Expression
169+
) => {
170+
node.type = 'MemberExpression'
171+
node = node as es.MemberExpression
172+
node.object = obj
173+
node.property = prop
174+
node.computed = false
175+
}
176+
141177
export const logicalExpression = (
142178
operator: es.LogicalOperator,
143179
left: es.Expression,

‎yarn.lock

+1,813-1,205
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.