1 /*
  2  * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 import job.*;
 27 
 28 static void logo(){
 29     System.out.println("""
 30         ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
 31         ⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀
 32         ⠀⠀⠀⠀⠀⠀⠀ ⠙⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠀⠀⠀⠀⠀⠀⠀
 33         ⠀⠀⠀⠀⠀⠀⠀⠀⣷⣶⣤⣄⣈⣉⣉⣉⣉⣉⣉⣉⣁⣤⡄⠀⠀⠀⠀⠀⠀⠀
 34         ⠀⠀⠀⠀⠀⠀ ⠀⣿⣿⣿⣿⣿ HAT ⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀
 35         ⠀⠀⢀⣠⣶⣾⡏⢀⡈⠛⠻⠿⢿⣿⣿⣿⣿⣿⠿⠿⠟⠛⢁⠀⢶⣤⣀⠀⠀⠀
 36         ⠀⢠⣿⣿⣿⣿⡇⠸⣿⣿⣶⣶⣤⣤⣤⣤⣤⣤⣤⣶⣶⣿⡿⠂⣸⣿⣿⣷⡄⠀
 37         ⠀⢸⣿⣿⣿⣿⣿⣦⣄⡉⠛⠛⠛⠿⠿⠿⠿⠛⠛⠛⢉⣁⣤⣾⣿⣿⣿⣿⡷⠀
 38         ⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⡿⠛⠁⠀
 39         ⠀⠀⠀⠀⠈⠙⠛⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⠛⠛⠉⠁⠀⠀⠀⠀
 40         """);
 41 }
 42 static void help(){
 43     System.out.println("""
 44         Usage  bld|clean|run ...
 45              bld:
 46                    Compile all buildable (based on capabilities) available jars and native code.
 47 
 48              dot:
 49                    Create dot graph  (bld.dot) of buildable dependencies (based on capabilities)
 50                       dot bld.dot -Tsvg > bld.svg  && chrome bld.svg
 51            clean:
 52                    Removes build directory entirely
 53                    conf dir and jextracted artifacts (opencl/cuda/opengl) remain
 54 
 55 
 56              run:  [ffi|my|seq]-[opencl|java|cuda|mock|hip] [-DXXX ...] runnable  args
 57                       run ffi-opencl mandel
 58                       run ffi-opencl nbody HAT
 59                       run ffi-opencl nbodygl 4096
 60                       run ffi-opencl -DHAT=SHOW_CODE nbodygl 4096
 61                       run ffi-opencl -DHAT=SHOW_KERNEL_MODEL heal
 62                       run ffi-opencl -DHAT=MINIMIZE_BUFFERS life
 63 
 64              exp:  [ffi|my|seq]-[opencl|java|cuda|mock|hip] [-DXXX ... ] experimentClassName  args
 65                       exp ffi-opencl QuotedConstantArgs
 66 
 67              test-suite:  [ffi|my|seq]-[opencl|java|cuda|mock|hip]
 68                       test-suite ffi-opencl
 69 
 70              test:  [ffi|my|seq]-[opencl|java|cuda|mock|hip] classToTest (also classToTest#method)
 71                       test ffi-opencl  hat.test.TestMatMul
 72 
 73           sanity:  Check source files for copyright and WS issues (tabs and trailing EOL WS)
 74         """);
 75 }
 76 
 77 static  void logoAndHelp(){
 78     logo();
 79     help();
 80 }
 81 
 82 private static class Colours {
 83     public static String RED = "\u001b[31m";
 84     public static String GREEN = "\u001b[32m";
 85     public static String BLUE = "\u001b[34m";
 86     public static String CYAN = "\u001b[36m";
 87     public static String YELLOW = "\u001b[33m";
 88     public static String RESET = "\u001b[0m";
 89 }
 90 
 91 public static void main(String[] argArr) throws IOException, InterruptedException {
 92     var args = new ArrayList<>(List.of(argArr));
 93     if (args.isEmpty()) {
 94         help();
 95     } else {
 96         var hat = new Project(
 97                 Util.currentDirAsPath(),
 98                 // These opts will be applied to all javac, cmake and jextract tools
 99                 Jar.JavacConfig.of(o -> o.progress(true)
100                         .debug().enablePreview().source(28).addModules("jdk.incubator.code")
101                 ),
102                 CMake.Config.of(o -> o.progress(true)),
103                 JExtract.Config.of(o -> o.progress(true))
104         );
105 
106         var commonJavaOpts = Jar.JavaConfig.of(o -> o
107                 .verbose(true)
108                 .command(true)
109                 .enablePreview()
110                 .addModules("jdk.incubator.code")
111                 .enableNativeAccess("ALL-UNNAMED"));
112 
113         var cmake = hat.isAvailable("cmake", "--version");
114         if (!cmake.isAvailable()) {
115             System.out.println("We need cmake, to check the availability of opencl, cuda etc so we wont be able to build much  ");
116         }
117         var jextract = hat.isAvailable("jextract", "--version");
118         if (!jextract.isAvailable()) {
119             System.out.println("We will need jextract to create jextracted backends and for examples requiring opengl ");
120         }
121 
122         // This is an example of a user defined optional dependency.
123         // Anything depending on this will only build if (In this case) the property is true
124         // So in our case if the headless system property
125         var ui = new Opt(hat.id("ui"), !Boolean.getBoolean("headless")); //-Dheadless=true|false
126 
127         // These dependencies are  'true' on the appropriate platform.
128         // So any target depending on one these, will only build on that platform
129         var mac = new Mac(hat.id("os-mac"));
130         var linux = new Linux(hat.id("os-linux"));
131 
132         // These next three 'optional' dependencies use cmake to determine availability.  We delegate to cmake which
133         //    a) determines if capability is available,
134         //    b) if they are, they extract from cmake vars (see conf/cmake-info/OpenCL/properties for example)
135         //       information export headers and libs needed by JExtract
136         var jextractOpts = JExtract.Config.of(o -> o.command(true));
137         var cmakeOpts = CMake.Config.of(o -> o.command(true));
138         var openclCmakeInfo = new OpenCL(hat.id("cmake-info-opencl"), cmake);
139         var openglCmakeInfo = new OpenGL(hat.id("cmake-info-opengl"), cmake);
140         var cudaCmakeInfo = new Cuda(hat.id("cmake-info-cuda"), cmake);
141 
142         // Now we just create jars and shared libs and declare dependencies
143         var optkl = hat.jar("optkl");
144         var core = hat.jar("core", optkl);
145         //var tools = hat.jar("tools", core);
146         var tests = hat.jar("tests", core);
147 
148         var backend_ffi_native = hat.cmakeAndJar("backend{s}-ffi", core, cmake);
149         var ffiSharedBackend = hat.jar("backend{s}-ffi-shared", backend_ffi_native);
150         var backend_ffi_cuda = hat.jar("backend{s}-ffi-cuda", ffiSharedBackend);
151         var backend_ffi_opencl = hat.jar("backend{s}-ffi-opencl", ffiSharedBackend);
152         var backend_ffi_mock = hat.jar("backend{s}-ffi-mock", ffiSharedBackend);
153 
154         // These examples just rely on core
155         var backend_mt_java = hat.jar("backend{s}-java-mt", core);
156         var backend_seq_java = hat.jar("backend{s}-java-seq", core);
157         var example_squares = hat.jar("example{s}-squares", core);
158         var example_blackscholes = hat.jar("example{s}-blackscholes", core);
159         var example_view = hat.jar("example{s}-view", core);
160         var example_normmap = hat.jar("example{s}-normmap", core); // will probabvly need shared when we hatify
161 
162         // example_shared allows us to break out common UI functions, views, even loops etc
163         var example_shared = hat.jar("example{s}-shared", ui, core);
164         var example_nbody = hat.jar("example{s}-nbody", ui, core);
165 
166         var example_flash_attention = hat.jar("example{s}-flashattention", core, example_shared);
167         var example_dft = hat.jar("example{s}-dft", core, example_shared);
168         var example_fft = hat.jar("example{s}-fft", core, example_shared);
169         var example_matmul = hat.jar("example{s}-matmul", core, example_shared);
170         var example_tensors = hat.jar("example{s}-tensors", core, example_shared);
171 
172         // These examples use example_shared, so they are UI based
173         var example_mandel = hat.jar("example{s}-mandel", example_shared);
174         var example_life = hat.jar("example{s}-life", example_shared);
175         var example_heal = hat.jar("example{s}-heal", example_shared);
176         var example_shade = hat.jar("example{s}-shade", example_shared);
177         var example_violajones = hat.jar("example{s}-violajones", example_shared);
178 
179         // experiments include code that expects an opencl backend, this is not idea, but we can accomodate
180         var example_experiments = hat.jar("example{s}-experiments", core);
181 
182         // Now we have the more complex nonsense for nbodygl (which needs opengl and opencl extracted)
183         var wrapped_shared = hat.jar("wrap{s}-shared");
184         var jextracted_opencl = hat.jextract("extract{ions|ed}-opencl", jextract, openclCmakeInfo, core);
185         var wrapped_jextracted_opencl = hat.jar("wrap{s}-opencl", jextracted_opencl, wrapped_shared);
186         var backend_jextracted_shared = hat.jar("backend{s}-jextracted-shared", core);
187         var backend_jextracted_opencl = hat.jar("backend{s}-jextracted-opencl", wrapped_jextracted_opencl, backend_jextracted_shared);
188         var jextracted_opengl = hat.jextract("extract{ions|ed}-opengl", jextract, ui, openglCmakeInfo, core);
189 
190         // Sigh... We have different src exclusions for wrapped opengl depending on the OS
191         var excludedOpenGLWrapSrc = hat.rootPath().resolve(
192                 "wraps/opengl/src/main/java/wrap/opengl/GL" + (mac.isAvailable() ? "Callback" : "Func") + "EventHandler.java");
193 
194         var wrapped_jextracted_opengl = hat.jar("wrap{s}-opengl", Set.of(excludedOpenGLWrapSrc), jextracted_opengl, wrapped_shared);
195 
196         // Finally we have everything needed for nbodygl
197         var example_nbodygl = hat.jar("example{s}-nbodygl", ui, wrapped_jextracted_opengl, wrapped_jextracted_opencl);
198 
199         var listOfExamples = List.of(
200            example_squares,
201            example_matmul,
202            example_flash_attention,
203            example_blackscholes,
204            example_view,
205            example_normmap,
206            example_nbody,
207            example_mandel,
208            example_life,
209            example_heal,
210            example_shade,
211            example_violajones,
212            example_experiments,
213            example_nbodygl
214         );
215         //listOfExamples.forEach(jar->System.out.println(jar.id().projectRelativeHyphenatedName()));
216         //System.exit(1);
217         var testEnginePackage = "hat.test.engine";
218         var testEngineClassName = "HATTestEngine";
219         while (!args.isEmpty()) {
220             var arg = args.removeFirst();
221             switch (arg) {
222                 case "help" -> logoAndHelp();
223                 case "clean" -> hat.clean(true);
224                 case "dot" -> {
225                     Files.writeString(Path.of("bld.dot"), hat.all().available().toDot());
226                     System.out.println("Consider...\n    dot bld.dot -Tsvg > bld.svg");
227                 }
228                 case "bld" -> {
229                     hat.build(hat.all().available());
230                 }
231                 case "sanity" -> {
232                     final var copyrightPattern = Pattern.compile("^.*Copyright.*202[0-9].*(Intel|Oracle).*$");
233                     final var copyrightExemptPattern = Pattern.compile("^(robertograham|CMakeFiles|hip)");
234                     final var tabOrEolWsPattern = Pattern.compile("^(.*\\t.*|.* )$");
235                     final var textSuffix = Pattern.compile("^(.*\\.(java|cpp|h|hpp|md)|pom.xml)$");
236                     final var sourceSuffix = Pattern.compile("^(.*\\.(java|cpp|h|hpp)|pom.xml)$");
237 
238                     Stream.of("hat", "tests", "optkl", "core", "examples", "backends", "docs", "wraps")
239                             .map(hat.rootPath()::resolve)
240                             .forEach(dir -> {
241                                 System.out.println("Checking " + dir);
242                                 Util.recurse(dir,
243                                         (d) -> true, // we do this for all subdirs
244                                         (f) -> textSuffix.matcher(f.getFileName().toString()).matches() && Util.grepLines(tabOrEolWsPattern, f),
245                                         (c) -> System.out.println("File contains WS issue (TAB or EOLWs) " + c)
246                                 );
247                                 Util.recurse(dir,
248                                         (d) -> !copyrightExemptPattern.matcher(d.getFileName().toString()).matches(), // we skip these subdirs
249                                         (f) -> sourceSuffix.matcher(f.getFileName().toString()).matches() && !Util.grepLines(copyrightPattern, f),
250                                         (c) -> System.out.println("File does not contain copyright " + c)
251                                 );
252                             });
253                     args.clear();
254                 }
255                 case "run" -> {
256                     if (args.size() > 1) {
257                         var backendName = args.removeFirst();
258                         if (hat.get(backendName) instanceof Jar backend) {
259                             var javaOpts = commonJavaOpts.with(o -> o
260                                     .collectVmOpts(args).mainClass(args.removeFirst(), "Main")
261                                     .startOnFirstThreadIf(o.packageName().equals("nbodygl") && mac.isAvailable()).collectArgs(args)
262                             );
263                             if (hat.get(javaOpts.packageName()) instanceof Jar runnable) {
264                                 runnable.run(javaOpts, runnable, backend);
265                             } else {
266                                 System.err.println("Found backend " + backendName + " but failed to find runnable/example " + javaOpts.packageName());
267                             }
268                         } else {
269                             System.err.println("Failed to find backend " + backendName);
270                         }
271                     } else {
272                         System.err.println("For run we expect 'run backend runnable' ");
273                     }
274                 }
275                 case "test-suite" -> {
276                     if (!args.isEmpty()) {
277                         var backendName = args.removeFirst();
278                         if (hat.get(backendName) instanceof Jar backend) {
279                             System.out.println("""
280                                     *****************************************************************
281                                     HAT Test Report
282                                     *****************************************************************
283                                     """);
284                             var test_reports_txt = Paths.get("test_report.txt");
285                             Files.deleteIfExists(test_reports_txt); // because we will append to it in the next loop
286                             var commonTestSuiteJavaOpts = commonJavaOpts.with(o -> o
287                                     .command(false).collectVmOpts(args).mainClass(testEnginePackage, testEngineClassName)
288                                 //  note no app args as add them below
289                             );
290 
291                             // First run - checking the total number of tests
292                             int[] totalTests = new int[]{0};
293                             tests.forEachMatchingEntry("(hat/test/Test[a-zA-Z0-9]*).class", (_, matcher) -> {
294                                 tests.run(Jar.JavaConfig.of(commonTestSuiteJavaOpts, o -> o.arg(matcher.group(1).replace('/', '.')).arg("--count-tests")), tests, backend);
295                                 Path path = Path.of(".num_tests");
296                                 try {
297                                     // accomulate the total number of tests
298                                     String content = Files.readString(path);
299                                     totalTests[0] += Integer.parseInt(content);
300                                 } catch (IOException e) {}
301                             });
302 
303                             // Second run - testing
304                             tests.forEachMatchingEntry("(hat/test/Test[a-zA-Z0-9]*).class", (_, matcher) ->
305                                     tests.run(Jar.JavaConfig.of(commonTestSuiteJavaOpts, o -> o.arg(matcher.group(1).replace('/', '.'))), tests, backend)
306                             );
307                             args.clear();
308                             var pattern = Pattern.compile("passed: (\\d+), failed: (\\d+), unsupported: (\\d+), precision-errors: (\\d+)");
309                             class Stats {
310                                 int passed = 0;
311                                 int failed = 0;
312                                 int unsupported = 0;
313                                 int precisionError = 0;
314 
315                                 @Override
316                                 public String toString() {
317                                     return String.format("Global passed: %d, failed: %d, unsupported: %d, precision-errors: %d, pass-rate: %.2f%%\\n",
318                                             passed, failed, unsupported, precisionError, ((float) (passed * 100 / (passed + failed + unsupported + precisionError))));
319                                 }
320                                 public int total() {
321                                     return passed + failed + unsupported + precisionError;
322                                 }
323                             }
324                             var stats = new Stats();
325                             Files.readAllLines(test_reports_txt).forEach(line -> {
326                                 if (!commonTestSuiteJavaOpts.verbose()) { //We already dumped this info to stdout above
327                                     System.out.println(line);
328                                 }
329                                 if (pattern.matcher(line) instanceof Matcher matcher && matcher.find()) {
330                                     stats.passed += Integer.parseInt(matcher.group(1));
331                                     stats.failed += Integer.parseInt(matcher.group(2));
332                                     stats.unsupported += Integer.parseInt(matcher.group(3));
333                                     stats.precisionError += Integer.parseInt(matcher.group(4));
334                                 }
335                             });
336 
337                             IO.println(Colours.BLUE);
338                             IO.println(stats);
339                             IO.println(Colours.RESET);
340 
341                             if (stats.total() == totalTests[0]) {
342                                 IO.println(Colours.GREEN);
343                                 IO.println("[REPORT] OK: All tests launched. Total: " + totalTests[0]);
344                                 IO.println(Colours.RESET);
345                             } else {
346                                 IO.println(Colours.RED);
347                                 IO.println("[REPORT] Test failed. Some tests were not launched. Common reasons: seg-faults, driver-issues. Please, check again.");
348                                 IO.println("    - Expected to run: " + totalTests[0] + ". But launched " + stats.total());
349                                 IO.println(Colours.RESET);
350                             }
351 
352                             if (stats.failed > 0) {
353                                 System.exit(-1);
354                             } else {
355                                 System.exit(0);
356                             }
357                         } else {
358                             System.err.println("Failed to find backend   " + backendName);
359                         }
360                     } else {
361                         System.err.println("For test-suite we require a backend ");
362                     }
363                 }
364                 case "test" -> {
365                     if (args.size() >= 2) {
366                         var backendName = args.removeFirst();
367                         if (hat.get(backendName) instanceof Jar backend) {
368                             var javaOpts = commonJavaOpts.with(o -> o
369                                     .verbose(true).command(true).collectVmOpts(args).mainClass(testEnginePackage, testEngineClassName).collectArgs(args)
370                             );
371                             tests.run(javaOpts, tests, backend);
372                         } else {
373                             System.err.println("Failed to find backend   " + backendName);
374                         }
375                     } else {
376                         System.err.println("""
377                                 For test we require a backend and a TestClass.");
378                                 Examples:
379                                     $ test ffi-opencl hat.test.TestMatMul
380                                     $ test ffi-opencl hat.test.TestMatMul#method
381                                 """);
382                     }
383                     args.clear(); //!! :)
384                 }
385                 case "exp" -> {
386                     if (args.size() > 1) {
387                         var backendName = args.removeFirst();
388                         if (hat.get(backendName) instanceof Jar backend) {
389                             var javaOpts = Jar.JavaConfig.of(commonJavaOpts, o -> o
390                                     .collectVmOpts(args).mainClass("experiments", args.removeFirst()).collectArgs(args)
391                             );
392                             if (hat.get(javaOpts.packageName()) instanceof Jar runnable) {
393                                 runnable.run(javaOpts, runnable, backend);
394                             } else {
395                                 System.err.println("Failed to find runnable " + javaOpts.mainClassName());
396                             }
397                         } else {
398                             System.err.println("Failed to find " + backendName);
399                         }
400                     } else {
401                         System.err.println("For exp we expect 'exp backend [-DXXX ...] testclass' ");
402                     }
403                     args.clear(); //!! :)
404                 }
405                 default -> {
406                     System.err.println("'" + arg + "' was unexpected ");
407                     help();
408                     args.clear();
409                 }
410             }
411         }
412     }
413 }