diff --git a/.github/workflows/macos-wasm.yml b/.github/workflows/macos-wasm.yml new file mode 100644 index 000000000..c2c5b1f62 --- /dev/null +++ b/.github/workflows/macos-wasm.yml @@ -0,0 +1,56 @@ +name: MacOS WASM + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-wasm: + name: WASM + runs-on: macos-10.15 + + steps: + - name: Setup Emscripten toolchain + uses: mymindstorm/setup-emsdk@v8 + + - name: Verify Emscripten setup + run: emcc -v + + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Configure builds + run: | + mkdir -p build-wasm + cd build-wasm + emcmake cmake \ + -DCOMPILE_CUDA=off \ + -DUSE_DOXYGEN=off \ + -DCOMPILE_EXAMPLES=off \ + -DCOMPILE_SERVER=off \ + -DCOMPILE_TESTS=off \ + -DUSE_FBGEMM=off \ + -DUSE_SENTENCEPIECE=on \ + -DUSE_STATIC_LIBS=on \ + -DUSE_MKL=off \ + -DCOMPILE_WASM=on ../ + + - name: Compile + working-directory: build-wasm + run: emmake make -j2 + + - name: Check artifacts + working-directory: build-wasm + run: | + ls -all . + if ls marian-decoder.wasm &>/dev/null && ls marian-decoder.js &>/dev/null + then + echo "Artifacts Successfully Generated" + else + echo "Failure: Artifacts Not Present" + exit 1 + fi diff --git a/.gitmodules b/.gitmodules index 9a54cf615..a8facd1fd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,7 @@ url = https://github.com/marian-nmt/marian-regression-tests [submodule "src/3rd_party/sentencepiece"] path = src/3rd_party/sentencepiece - url = https://github.com/marian-nmt/sentencepiece + url = https://github.com/browsermt/sentencepiece [submodule "src/3rd_party/nccl"] path = src/3rd_party/nccl url = https://github.com/marian-nmt/nccl @@ -16,7 +16,10 @@ branch = master [submodule "src/3rd_party/intgemm"] path = src/3rd_party/intgemm - url = https://github.com/kpu/intgemm/ + url = https://github.com/kpu/intgemm [submodule "src/3rd_party/simple-websocket-server"] path = src/3rd_party/simple-websocket-server url = https://github.com/marian-nmt/Simple-WebSocket-Server +[submodule "src/3rd_party/onnxjs"] + path = src/3rd_party/onnxjs + url = https://github.com/abhi-agg/onnxjs.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b58a618..29e22ec49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Quantized training (fixed point or log-based quantization) with --quantize-bits N command - Support for loading lexical shortlist from a binary blob - Integrate a shortlist converter (which can convert a text lexical shortlist to a binary shortlist) into marian-conv with --shortlist option +- Added ONNXJS submodule to use its wasm-compatible sgemm routine for wasm builds +- Enable compiling marian on wasm platform ### Fixed - Segfault of spm_train when compiled with -DUSE_STATIC_LIBS=ON seems to have gone away with update to newer SentencePiece version. @@ -55,8 +57,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added default "none" for option shuffle in BatchGenerator, so that it works in executables where shuffle is not an option. - Added a few missing header files in shortlist.h and beam_search.h. - Improved handling for receiving SIGTERM during training. By default, SIGTERM triggers 'save (now) and exit'. Prior to this fix, batch pre-fetching did not check for this sigal, potentially delaying exit considerably. It now pays attention to that. Also, the default behaviour of save-and-exit can now be disabled on the command line with --sigterm exit-immediately. +- Fix the runtime failures for FASTOPT on 32-bit builds (wasm just happens to be 32-bit) because it uses hashing with an inconsistent mix of uint64_t and size_t. +- Fix loading the binary model on 32-bit builds and for wasm platform ### Changed +- Updated intgemm repository to version 1a176394bb0c2d243c42fe574e063924a92aa120 from https://github.com/kpu/intgemm. +- Changed SentencePiece repository from https://github.com/google/sentencepiece to the version 3ffdc0065a03cadd9d0e5e123aaf9b6ea7ffb05d of https://github.com/browsermt/sentencepiece. - Updated SentencePiece repository to version 8336bbd0c1cfba02a879afe625bf1ddaf7cd93c5 from https://github.com/google/sentencepiece. - Enabled compilation of SentencePiece by default since no dependency on protobuf anymore. - Changed default value of --sentencepiece-max-lines from 10000000 to 2000000 since apparently the new version doesn't sample automatically anymore (Not quite clear how that affects quality of the vocabulary). @@ -68,6 +74,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Changed compile flags -Ofast to -O3 and remove --ffinite-math - Moved old graph groups to depracated folder - Make cublas and cusparse handle inits lazy to save memory when unused +- Replaced exception-based implementation for type determination in FastOpt::makeScalar ## [1.9.0] - 2020-03-10 diff --git a/CMakeLists.txt b/CMakeLists.txt index 92bf3c379..ba6892b44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,8 @@ endif () project(marian CXX C) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(BUILD_ARCH native CACHE STRING "Compile for this CPU architecture.") + +include(CMakeDependentOption) # Custom CMake options option(COMPILE_CPU "Compile CPU version" ON) @@ -26,6 +27,41 @@ option(USE_NCCL "Use NCCL library" ON) option(USE_SENTENCEPIECE "Download and compile SentencePiece" ON) option(USE_STATIC_LIBS "Link statically against non-system libs" OFF) option(GENERATE_MARIAN_INSTALL_TARGETS "Generate Marian install targets (requires CMake 3.12+)" OFF) +option(M32_BINARIES "Generate 32bit binaries even when building outside of WASM. Useful for testing some WASM specific functionality without the need for the compiling to WASM." OFF) +option(COMPILE_WASM "Compile customized marian for WASM target" OFF) + +# cmake options that are dependent on COMPILE_WASM cmake option +CMAKE_DEPENDENT_OPTION(USE_THREADS "Compile with multi-threading support" OFF + "COMPILE_WASM" ON) +CMAKE_DEPENDENT_OPTION(USE_WASM_COMPATIBLE_BLAS "Compile with wasm compatible blas" ON + "COMPILE_WASM" OFF) +CMAKE_DEPENDENT_OPTION(COMPILE_WITHOUT_EXCEPTIONS "Compile without exceptions" ON + "COMPILE_WASM" OFF) + +if (COMPILE_WASM) + set(WORMHOLE ON CACHE BOOL "Use WASM wormhole in intgemm https://bugzilla.mozilla.org/show_bug.cgi?id=1672160") +endif() + +if(M32_BINARIES OR COMPILE_WASM) + set("BUILD_WIDTH" "-m32") +else(M32_BINARIES OR COMPILE_WASM) + set("BUILD_WIDTH" "-m64") +endif() + +if(NOT COMPILE_WASM) + # Setting BUILD_ARCH to native invokes CPU intrinsic detection logic below. + # Prevent invoking that logic for WASM builds. + set(BUILD_ARCH native CACHE STRING "Compile for this CPU architecture.") +endif() + +if(USE_THREADS) + # Need to set compile definition as well + add_compile_definitions(USE_PTHREADS) +endif() + +if(COMPILE_WITHOUT_EXCEPTIONS) + add_compile_definitions(WITHOUT_EXCEPTIONS) +endif() # fbgemm and sentencepiece are both defined with "non-local" installation targets (the source projects don't define them, # so we define them in src\3rd_party\CMakeLists.txt), but that isn't supported until CMake 3.12. Prior to CMake 3.12, @@ -165,6 +201,10 @@ else(MSVC) set(INTRINSICS "${INTRINSICS} -mavx512f") list(APPEND INTRINSICS_NVCC -Xcompiler\ -mavx512f) endif(AVX512_FOUND) + elseif(COMPILE_WASM) + # Can't set to -msse4.1 because onnxjs doesn't compile with this flag. It can be upgraded to + # -msse4.1 once marian can solely be compiled with intgemm ("onnxjs" will be removed in that case) + set(INTRINSICS "-mssse3 -msimd128") else() set(INTRINSICS "-msse4.1") endif() @@ -197,24 +237,44 @@ else(MSVC) set(CMAKE_RDYNAMIC_FLAG "-rdynamic") endif(CMAKE_COMPILER_IS_GNUCC) - set(CMAKE_CXX_FLAGS "-std=c++11 -pthread ${CMAKE_GCC_FLAGS} -fPIC ${DISABLE_GLOBALLY} -march=${BUILD_ARCH} ${INTRINSICS}") - set(CMAKE_CXX_FLAGS_RELEASE "-O3 -m64 -funroll-loops -g ${CMAKE_RDYNAMIC_FLAG}") - set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g ${CMAKE_RDYNAMIC_FLAG}") - set(CMAKE_CXX_FLAGS_SLIM "-O3 -m64 -funroll-loops -DNDEBUG") - set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE}") - set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE} -pg") - set(CMAKE_CXX_FLAGS_PROFGEN "${CMAKE_CXX_FLAGS_RELEASE} -fprofile-generate -fprofile-correction") - set(CMAKE_CXX_FLAGS_PROFUSE "${CMAKE_CXX_FLAGS_RELEASE} -fprofile-use -fprofile-correction") - - # these need to be set separately - set(CMAKE_C_FLAGS "-pthread ${CMAKE_GCC_FLAGS} -fPIC ${DISABLE_GLOBALLY} -march=${BUILD_ARCH} ${INTRINSICS}") - set(CMAKE_C_FLAGS_RELEASE "-O3 -m64 -funroll-loops -g ${CMAKE_RDYNAMIC_FLAG}") - set(CMAKE_C_FLAGS_DEBUG "-O0 -g ${CMAKE_RDYNAMIC_FLAG}") - set(CMAKE_C_FLAGS_SLIM "-O3 -m64 -funroll-loops -DNDEBUG") - set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELEASE}") - set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE} -pg") - set(CMAKE_C_FLAGS_PROFGEN "${CMAKE_C_FLAGS_RELEASE} -fprofile-generate -fprofile-correction") - set(CMAKE_C_FLAGS_PROFUSE "${CMAKE_C_FLAGS_RELEASE} -fprofile-use -fprofile-correction") + if(COMPILE_WASM) + # Setting USE_SSE2 definition to enable SSE2 specific code in "3rd_party/sse_mathfun.h" for wasm builds + add_compile_definitions(USE_SSE2) + # Add compile definition for wasm builds + add_compile_definitions(WASM) + set(PTHREAD_FLAG "-pthread") + set(DISABLE_PTHREAD_MEMGROWTH_WARNING -Wno-error=pthreads-mem-growth) + set(CMAKE_CXX_FLAGS "-std=c++11 ${PTHREAD_FLAG} ${CMAKE_GCC_FLAGS} -fPIC ${DISABLE_GLOBALLY} ${INTRINSICS}") + set(CMAKE_CXX_FLAGS_RELEASE "-O3 -s WASM=1 -s ASSERTIONS=0 -s DISABLE_EXCEPTION_CATCHING=1 -s LLD_REPORT_UNDEFINED -s FORCE_FILESYSTEM=1 -s ALLOW_MEMORY_GROWTH=1 -g2 ${DISABLE_PTHREAD_MEMGROWTH_WARNING} -funroll-loops") + # Disabling Pthreads + memory growth warning to be an error + # Pthreads + memory growth causes JS accessing the wasm memory to be slow + # https://github.com/WebAssembly/design/issues/1271 + list(APPEND ALL_WARNINGS ${DISABLE_PTHREAD_MEMGROWTH_WARNING}) + + # use our customizations to the generated emscripted html and js resources + set(MARIAN_DECODER_EMSCRIPTEN_LINK_FLAGS "--pre-js ${CMAKE_SOURCE_DIR}/wasm/pre-module.js \ + --post-js ${CMAKE_SOURCE_DIR}/wasm/post-module.js \ + --shell-file ${CMAKE_SOURCE_DIR}/wasm/custom_shell.html") + else(COMPILE_WASM) + set(CMAKE_CXX_FLAGS "-std=c++11 -pthread ${CMAKE_GCC_FLAGS} -fPIC ${DISABLE_GLOBALLY} -march=${BUILD_ARCH} ${INTRINSICS} ${BUILD_WIDTH}") + set(CMAKE_CXX_FLAGS_RELEASE "-O3 -funroll-loops -g ${CMAKE_RDYNAMIC_FLAG}") + set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g ${CMAKE_RDYNAMIC_FLAG}") + set(CMAKE_CXX_FLAGS_SLIM "-O3 ${BUILD_WIDTH} -funroll-loops -DNDEBUG") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE}") + set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE} -pg") + set(CMAKE_CXX_FLAGS_PROFGEN "${CMAKE_CXX_FLAGS_RELEASE} -fprofile-generate -fprofile-correction") + set(CMAKE_CXX_FLAGS_PROFUSE "${CMAKE_CXX_FLAGS_RELEASE} -fprofile-use -fprofile-correction") + + # these need to be set separately + set(CMAKE_C_FLAGS "-pthread ${CMAKE_GCC_FLAGS} -fPIC ${DISABLE_GLOBALLY} -march=${BUILD_ARCH} ${INTRINSICS}") + set(CMAKE_C_FLAGS_RELEASE "-O3 ${BUILD_WIDTH} -funroll-loops -g ${CMAKE_RDYNAMIC_FLAG}") + set(CMAKE_C_FLAGS_DEBUG "-O0 -g ${CMAKE_RDYNAMIC_FLAG}") + set(CMAKE_C_FLAGS_SLIM "-O3 ${BUILD_WIDTH} -funroll-loops -DNDEBUG") + set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELEASE}") + set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE} -pg") + set(CMAKE_C_FLAGS_PROFGEN "${CMAKE_C_FLAGS_RELEASE} -fprofile-generate -fprofile-correction") + set(CMAKE_C_FLAGS_PROFUSE "${CMAKE_C_FLAGS_RELEASE} -fprofile-use -fprofile-correction") + endif(COMPILE_WASM) endif(MSVC) # with gcc 7.0 and above we need to mark fallthrough in switch case statements @@ -435,29 +495,37 @@ if(USE_MPI) endif(USE_MPI) ############################################################################### -# Find MKL +# Find BLAS library for CPU compilation if(COMPILE_CPU) - set(EXT_LIBS ${EXT_LIBS} intgemm) - if(USE_MKL) - find_package(MKL) - endif(USE_MKL) - if(MKL_FOUND) - include_directories(${MKL_INCLUDE_DIR}) - set(EXT_LIBS ${EXT_LIBS} ${MKL_LIBRARIES}) + if(USE_WASM_COMPATIBLE_BLAS) + ## Use a wasm compatible BLAS + set(EXT_LIBS ${EXT_LIBS} intgemm onnx-sgemm) set(BLAS_FOUND TRUE) - add_definitions(-DCOMPILE_CPU=1 -DBLAS_FOUND=1 -DMKL_FOUND=1) - else(MKL_FOUND) - set(BLAS_VENDOR "OpenBLAS") - find_package(BLAS) - if(BLAS_FOUND) - include(FindCBLAS) - if(CBLAS_FOUND) - include_directories(${BLAS_INCLUDE_DIR} ${CBLAS_INCLUDE_DIR}) - set(EXT_LIBS ${EXT_LIBS} ${BLAS_LIBRARIES} ${CBLAS_LIBRARIES}) - add_definitions(-DCOMPILE_CPU=1 -DBLAS_FOUND=1) - endif(CBLAS_FOUND) - endif(BLAS_FOUND) - endif(MKL_FOUND) + set(BLAS_VENDOR "ONNX-SGEMM") + add_compile_definitions(COMPILE_CPU BLAS_FOUND WASM_COMPATIBLE_BLAS) + else(USE_WASM_COMPATIBLE_BLAS) + set(EXT_LIBS ${EXT_LIBS} intgemm) + if(USE_MKL) + find_package(MKL) + endif(USE_MKL) + if(MKL_FOUND) + include_directories(${MKL_INCLUDE_DIR}) + set(EXT_LIBS ${EXT_LIBS} ${MKL_LIBRARIES}) + set(BLAS_FOUND TRUE) + add_definitions(-DCOMPILE_CPU=1 -DBLAS_FOUND=1 -DMKL_FOUND=1) + else(MKL_FOUND) + set(BLAS_VENDOR "OpenBLAS") + find_package(BLAS) + if(BLAS_FOUND) + include(FindCBLAS) + if(CBLAS_FOUND) + include_directories(${BLAS_INCLUDE_DIR} ${CBLAS_INCLUDE_DIR}) + set(EXT_LIBS ${EXT_LIBS} ${BLAS_LIBRARIES} ${CBLAS_LIBRARIES}) + add_definitions(-DCOMPILE_CPU=1 -DBLAS_FOUND=1) + endif(CBLAS_FOUND) + endif(BLAS_FOUND) + endif(MKL_FOUND) + endif(USE_WASM_COMPATIBLE_BLAS) endif(COMPILE_CPU) ############################################################################### diff --git a/src/3rd_party/CMakeLists.txt b/src/3rd_party/CMakeLists.txt index f5314b12b..0869ade6b 100644 --- a/src/3rd_party/CMakeLists.txt +++ b/src/3rd_party/CMakeLists.txt @@ -2,15 +2,21 @@ include_directories(.) add_subdirectory(./yaml-cpp) -add_subdirectory(./SQLiteCpp) +if(NOT COMPILE_WASM) + add_subdirectory(./SQLiteCpp) + add_subdirectory(./zlib) + add_subdirectory(./faiss) + include_directories(./faiss) +endif() add_subdirectory(./pathie-cpp) -add_subdirectory(./zlib) -add_subdirectory(./faiss) -include_directories(./faiss) set(INTGEMM_DONT_BUILD_TESTS ON CACHE BOOL "Disable intgemm tests") add_subdirectory(./intgemm) +if(USE_WASM_COMPATIBLE_BLAS) + add_subdirectory(./onnxjs) +endif(USE_WASM_COMPATIBLE_BLAS) + if(USE_FBGEMM) # @TODO: find out if this is somehow harmful. This is supppressing CMake warnings for CMAKE_SUPPRESS_DEVELOPER_WARNINGS # meant to silence CMakeFiles of 3rd_party tools. @@ -78,7 +84,6 @@ if(USE_SENTENCEPIECE) add_subdirectory(./sentencepiece) include_directories(./sentencepiece) - set_target_properties(spm_encode spm_decode spm_train spm_normalize spm_export_vocab PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") @@ -123,6 +128,7 @@ include_directories(./CLI) include_directories(./pathie-cpp/include) if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + if(NOT COMPILE_WASM) #set_target_properties(SQLiteCpp PROPERTIES COMPILE_FLAGS set_property(TARGET SQLiteCpp APPEND_STRING PROPERTY COMPILE_FLAGS " -Wno-parentheses-equality -Wno-unused-value") @@ -130,6 +136,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") set_property(TARGET SQLiteCpp APPEND_STRING PROPERTY COMPILE_FLAGS " -Wno-implicit-int-float-conversion") endif() + endif() set_property(TARGET libyaml-cpp APPEND_STRING PROPERTY COMPILE_FLAGS " -fPIC -Wno-unused-value") set_property(TARGET pathie-cpp APPEND_STRING PROPERTY COMPILE_FLAGS diff --git a/src/3rd_party/cnpy/cnpy.h b/src/3rd_party/cnpy/cnpy.h index 89e607cab..0ec94e3c2 100644 --- a/src/3rd_party/cnpy/cnpy.h +++ b/src/3rd_party/cnpy/cnpy.h @@ -5,7 +5,9 @@ #ifndef LIBCNPY_H_ #define LIBCNPY_H_ +#if !defined(WASM) #include "3rd_party/zlib/zlib.h" +#endif #include #include @@ -18,7 +20,7 @@ #include #include -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__unix__) #include #endif @@ -133,6 +135,9 @@ namespace cnpy { template void npz_save(std::string zipname, std::string fname, const T* data, const unsigned int* shape, const unsigned int ndims, std::string mode = "w") { +#if defined(WASM) + throw std::runtime_error("npz_save() not supported in WASM builds"); +#else //first, append a .npy to the fname fname += ".npy"; @@ -221,6 +226,7 @@ namespace cnpy { fwrite(&footer[0],sizeof(char),footer.size(),fp); //BUGBUG: no check for write error fclose(fp); +#endif } //one item pass to npz_save() below @@ -265,6 +271,9 @@ namespace cnpy { static inline void npz_save(std::string zipname, const std::vector& items) { +#if defined(WASM) + throw std::runtime_error("npz_save() not supported in WASM builds"); +#else auto tmpname = zipname + "$$"; // TODO: add thread id or something unlink(tmpname.c_str()); // when saving to HDFS, we cannot overwrite an existing file FILE* fp = fopen(tmpname.c_str(),"wb"); @@ -366,6 +375,7 @@ namespace cnpy { unlink(tmpname.c_str()); throw std::runtime_error("npz_save: error saving to file: " + zipname); } +#endif } static inline diff --git a/src/3rd_party/half_float/umHalf.inl b/src/3rd_party/half_float/umHalf.inl index cdccd8473..d24f14e01 100644 --- a/src/3rd_party/half_float/umHalf.inl +++ b/src/3rd_party/half_float/umHalf.inl @@ -45,11 +45,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #pragma intrinsic(_BitScanReverse) #endif -#if __cplusplus >= 201703L -#define __SWITCHED_REGISTER -#else -#define __SWITCHED_REGISTER register -#endif // ------------------------------------------------------------------------------------------------ inline HalfFloat::HalfFloat(float other) @@ -350,7 +345,7 @@ inline HalfFloat operator+ (HalfFloat one, HalfFloat two) // compute the difference between the two exponents. shifts with negative // numbers are undefined, thus we need two code paths - __SWITCHED_REGISTER int expDiff = one.IEEE.Exp - two.IEEE.Exp; + int expDiff = one.IEEE.Exp - two.IEEE.Exp; if (0 == expDiff) { diff --git a/src/3rd_party/intgemm b/src/3rd_party/intgemm index 84a8a1018..1a176394b 160000 --- a/src/3rd_party/intgemm +++ b/src/3rd_party/intgemm @@ -1 +1 @@ -Subproject commit 84a8a1018d8f5dc13afd0f103a0bcf3e5dd1dec0 +Subproject commit 1a176394bb0c2d243c42fe574e063924a92aa120 diff --git a/src/3rd_party/onnxjs b/src/3rd_party/onnxjs new file mode 160000 index 000000000..dfefde914 --- /dev/null +++ b/src/3rd_party/onnxjs @@ -0,0 +1 @@ +Subproject commit dfefde914fcc79b4c0f9eafcfc97e4b606af700e diff --git a/src/3rd_party/pathie-cpp/src/path.cpp b/src/3rd_party/pathie-cpp/src/path.cpp index e732e09c5..480422589 100644 --- a/src/3rd_party/pathie-cpp/src/path.cpp +++ b/src/3rd_party/pathie-cpp/src/path.cpp @@ -935,6 +935,8 @@ Path Path::exe() std::string str = utf16_to_utf8(buf); return Path(str); +#elif defined(WASM) + throw(std::runtime_error("Path::exe() not supported in WASM builds")); #else #error Unsupported platform. #endif diff --git a/src/3rd_party/sentencepiece b/src/3rd_party/sentencepiece index 8336bbd0c..3ffdc0065 160000 --- a/src/3rd_party/sentencepiece +++ b/src/3rd_party/sentencepiece @@ -1 +1 @@ -Subproject commit 8336bbd0c1cfba02a879afe625bf1ddaf7cd93c5 +Subproject commit 3ffdc0065a03cadd9d0e5e123aaf9b6ea7ffb05d diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f1a634695..e3c5e0c71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,13 +39,11 @@ set(MARIAN_SOURCES data/factored_vocab.cpp data/corpus_base.cpp data/corpus.cpp - data/corpus_sqlite.cpp data/corpus_nbest.cpp data/text_input.cpp data/shortlist.cpp 3rd_party/cnpy/cnpy.cpp - 3rd_party/ExceptionWithCallStack.cpp 3rd_party/onnx/protobuf/onnx-ml.pb-wrapper.cpp @@ -60,7 +58,6 @@ set(MARIAN_SOURCES tensors/cpu/tensor_operators.cpp tensors/cpu/integer_common.cpp - tensors/cpu/fbgemm/packed_gemm.cpp graph/expression_graph.cpp graph/expression_operators.cpp @@ -75,12 +72,10 @@ set(MARIAN_SOURCES layers/generic.cpp layers/loss.cpp layers/weight.cpp - layers/lsh.cpp rnn/cells.cpp rnn/attention.cpp - optimizers/quantizer.cpp optimizers/clippers.cpp optimizers/optimizers.cpp @@ -99,24 +94,35 @@ set(MARIAN_SOURCES translator/helpers.cpp translator/scorers.cpp - training/graph_group_async.cpp - training/graph_group_sync.cpp - training/graph_group.cpp - training/graph_group_singleton.cpp - training/validator.cpp - training/communicator.cpp - - # this is only compiled to catch build errors, but not linked - microsoft/quicksand.cpp - microsoft/cosmos.cpp - $ - $ $ - $ - $ ) +if (NOT COMPILE_WASM) + list(APPEND MARIAN_SOURCES + 3rd_party/ExceptionWithCallStack.cpp + + data/corpus_sqlite.cpp + tensors/cpu/fbgemm/packed_gemm.cpp + layers/lsh.cpp + optimizers/quantizer.cpp + + training/graph_group_async.cpp + training/graph_group_sync.cpp + training/graph_group.cpp + training/graph_group_singleton.cpp + training/validator.cpp + training/communicator.cpp + + microsoft/quicksand.cpp + microsoft/cosmos.cpp + + $ + $ + $ + ) +endif() + add_library(marian STATIC ${MARIAN_SOURCES}) target_compile_options(marian PRIVATE ${ALL_WARNINGS}) @@ -197,14 +203,23 @@ endif(CUDA_FOUND) # as a sub-project of another build system that is only interested in the Marian output library. option(COMPILE_LIBRARY_ONLY "Build only the Marian library and exclude all executables." OFF) if (NOT COMPILE_LIBRARY_ONLY) - add_executable(marian_train command/marian_main.cpp) - set_target_properties(marian_train PROPERTIES OUTPUT_NAME marian) - target_compile_options(marian_train PRIVATE ${ALL_WARNINGS}) - add_executable(marian_decoder command/marian_decoder.cpp) set_target_properties(marian_decoder PROPERTIES OUTPUT_NAME marian-decoder) target_compile_options(marian_decoder PRIVATE ${ALL_WARNINGS}) + set(EXECUTABLES ${EXECUTABLES} marian_decoder) + + if(COMPILE_WASM) + set_target_properties(marian_decoder PROPERTIES + LINK_FLAGS "${MARIAN_DECODER_EMSCRIPTEN_LINK_FLAGS}" + SUFFIX ".html") + endif(COMPILE_WASM) + + if (NOT COMPILE_WASM) + add_executable(marian_train command/marian_main.cpp) + set_target_properties(marian_train PROPERTIES OUTPUT_NAME marian) + target_compile_options(marian_train PRIVATE ${ALL_WARNINGS}) + add_executable(marian_scorer command/marian_scorer.cpp) set_target_properties(marian_scorer PROPERTIES OUTPUT_NAME marian-scorer) target_compile_options(marian_scorer PRIVATE ${ALL_WARNINGS}) @@ -217,7 +232,7 @@ if (NOT COMPILE_LIBRARY_ONLY) set_target_properties(marian_conv PROPERTIES OUTPUT_NAME marian-conv) target_compile_options(marian_conv PRIVATE ${ALL_WARNINGS}) - set(EXECUTABLES ${EXECUTABLES} marian_train marian_decoder marian_scorer marian_vocab marian_conv) + list(APPEND EXECUTABLES marian_train marian_scorer marian_vocab marian_conv) # marian.zip and marian.tgz # This combines marian, marian_decoder in a single ZIP or TAR file for @@ -261,6 +276,7 @@ if (NOT COMPILE_LIBRARY_ONLY) endif(MSVC) set(EXECUTABLES ${EXECUTABLES} marian_server) endif(COMPILE_SERVER) + endif(NOT COMPILE_WASM) foreach(exec ${EXECUTABLES}) target_link_libraries(${exec} marian) diff --git a/src/common/binary.cpp b/src/common/binary.cpp index fa8ffa110..09a276380 100644 --- a/src/common/binary.cpp +++ b/src/common/binary.cpp @@ -13,28 +13,28 @@ namespace io { namespace binary { struct Header { - size_t nameLength; - size_t type; - size_t shapeLength; - size_t dataLength; + uint64_t nameLength; + uint64_t type; + uint64_t shapeLength; + uint64_t dataLength; }; // cast current void pointer to T pointer and move forward by num elements template -const T* get(const void*& current, size_t num = 1) { +const T* get(const void*& current, uint64_t num = 1) { const T* ptr = (const T*)current; current = (const T*)current + num; return ptr; } void loadItems(const void* current, std::vector& items, bool mapped) { - size_t binaryFileVersion = *get(current); + uint64_t binaryFileVersion = *get(current); ABORT_IF(binaryFileVersion != BINARY_FILE_VERSION, "Binary file versions do not match: {} (file) != {} (expected)", binaryFileVersion, BINARY_FILE_VERSION); - size_t numHeaders = *get(current); // number of item headers that follow + uint64_t numHeaders = *get(current); // number of item headers that follow const Header* headers = get
(current, numHeaders); // read that many headers // prepopulate items with meta data from headers @@ -47,14 +47,14 @@ void loadItems(const void* current, std::vector& items, bool mapped) { // read in actual shape and data for(int i = 0; i < numHeaders; ++i) { - size_t len = headers[i].shapeLength; + uint64_t len = headers[i].shapeLength; items[i].shape.resize(len); const int* arr = get(current, len); // read shape std::copy(arr, arr + len, items[i].shape.begin()); // copy to Item::shape } // move by offset bytes, aligned to 256-bytes boundary - size_t offset = *get(current); + uint64_t offset = *get(current); get(current, offset); for(int i = 0; i < numHeaders; ++i) { @@ -62,7 +62,7 @@ void loadItems(const void* current, std::vector& items, bool mapped) { ABORT_IF(isIntgemm(items[i].type), "mmap format not supported for intgemm matrices"); items[i].ptr = get(current, headers[i].dataLength); } else { // reading into item data - size_t len = headers[i].dataLength; + uint64_t len = headers[i].dataLength; items[i].bytes.resize(len); const char* ptr = get(current, len); if (matchType(items[i].type)) { @@ -90,7 +90,7 @@ void loadItems(const void* current, std::vector& items, bool mapped) { void loadItems(const std::string& fileName, std::vector& items) { // Read file into buffer - size_t fileSize = filesystem::fileSize(fileName); + uint64_t fileSize = filesystem::fileSize(fileName); std::vector buf(fileSize); // @TODO: check this again: #if 1 // for some reason, the #else branch fails with "file not found" in the *read* operation (open succeeds) @@ -133,20 +133,20 @@ io::Item getItem(const std::string& fileName, const std::string& varName) { void saveItems(const std::string& fileName, const std::vector& items) { io::OutputFileStream out(fileName); - size_t pos = 0; + uint64_t pos = 0; - size_t binaryFileVersion = BINARY_FILE_VERSION; + uint64_t binaryFileVersion = BINARY_FILE_VERSION; pos += out.write(&binaryFileVersion); std::vector
headers; for(const auto& item : items) { headers.push_back(Header{item.name.size() + 1, - (size_t)item.type, + (uint64_t)item.type, item.shape.size(), item.bytes.size()}); // binary item size with padding, will be 256-byte-aligned } - size_t headerSize = headers.size(); + uint64_t headerSize = headers.size(); pos += out.write(&headerSize); pos += out.write(headers.data(), headers.size()); @@ -160,11 +160,11 @@ void saveItems(const std::string& fileName, } // align to next 256-byte boundary - size_t nextpos = ((pos + sizeof(size_t)) / 256 + 1) * 256; - size_t offset = nextpos - pos - sizeof(size_t); + uint64_t nextpos = ((pos + sizeof(uint64_t)) / 256 + 1) * 256; + uint64_t offset = nextpos - pos - sizeof(uint64_t); pos += out.write(&offset); - for(size_t i = 0; i < offset; i++) { + for(uint64_t i = 0; i < offset; i++) { char padding = 0; pos += out.write(&padding); } diff --git a/src/common/config_parser.cpp b/src/common/config_parser.cpp index 608aa8c84..59d47a741 100755 --- a/src/common/config_parser.cpp +++ b/src/common/config_parser.cpp @@ -17,10 +17,12 @@ #if MKL_FOUND #include -#else -#if BLAS_FOUND -#include -#endif +#elif BLAS_FOUND + #if WASM_COMPATIBLE_BLAS + #include "3rd_party/onnxjs/src/wasm-ops/gemm.h" + #else + #include + #endif // WASM_COMPATIBLE_BLAS #endif namespace marian { diff --git a/src/common/fastopt.cpp b/src/common/fastopt.cpp index 9af8e8445..e6992b71e 100644 --- a/src/common/fastopt.cpp +++ b/src/common/fastopt.cpp @@ -94,6 +94,7 @@ template struct As>; // Windows: size_t = unsigned long long (8 bytes), uint64_t = unsigned long long (8 bytes) template struct As>; template struct As>; +template struct As>; template struct As>; template struct As>; template struct As>; diff --git a/src/common/fastopt.h b/src/common/fastopt.h index 3f735660b..93b1afe6b 100644 --- a/src/common/fastopt.h +++ b/src/common/fastopt.h @@ -38,9 +38,10 @@ inline constexpr uint64_t crc(const char* const str) noexcept { /*****************************************************************************/ // PerfectHash constructs a perfect hash for a set K of n numeric keys. The size of -// the hash is m > n (not much larger) and n << max(K) (much smaller). If I am not wrong m -// is the next power of 2 larger than n? We then build an array of size m with n fields defined. -// m - n fields stay undefined (a bit of waste). +// the hash is m > n (not much larger) and n << max(K) (much smaller). The output array size is +// determined by PHF::init in "src/3rd_party/phf/phf.h". m - n fields stay undefined (a bit of waste). + +// Wrapper class for the 3rd-party library in "src/3rd_party/phf" class PerfectHash { private: phf phf_; @@ -62,10 +63,12 @@ class PerfectHash { PHF::destroy(&phf_); } + // subscript operator [] overloading: if the key is uint64_t, return the hash code directly uint32_t operator[](const uint64_t& key) const { return PHF::hash(const_cast(&phf_), key); } + // If the key is a string, return the hash code for the string's CRC code uint32_t operator[](const char* const keyStr) const { return (*this)[crc::crc(keyStr)]; } @@ -109,6 +112,9 @@ class FastOpt { public: // Node types for FastOpt, seem to be enough to cover YAML:NodeType + // Multi-element types include "Sequence" and "Map" + // "Sequence" is implemented with STL vectors + // "Map" is implemented with a 3rd-party PHF library (see the PerfectHash class) enum struct NodeType { Null, Bool, Int64, Float64, String, Sequence, Map }; @@ -126,6 +132,7 @@ class FastOpt { size_t elements_{0}; // Number of elements if isMap or isSequence is true, 0 otherwise. // Used to find elements if isSequence() is true. + // Retrieve the entry using array indexing. inline const std::unique_ptr& arrayLookup(size_t keyId) const { if(keyId < array_.size()) return array_[keyId]; @@ -134,13 +141,15 @@ class FastOpt { } // Used to find elements if isMap() is true. - inline const std::unique_ptr& phLookup(size_t keyId) const { + // Retrieve the entry from the hash table. + inline const std::unique_ptr& phLookup(uint64_t keyId) const { if(ph_) return array_[(*ph_)[keyId]]; else return uniqueNullPtr; } + // Builders for different types of nodes. // Build Null node. void makeNull() { elements_ = 0; @@ -153,35 +162,33 @@ class FastOpt { // Build Scalar node via controlled failure to convert from a YAML::Node object. void makeScalar(const YAML::Node& v) { elements_ = 0; - try { - // Cast node to text first, that works for any scalar node and test that it does not contain single characters - // that according to YAML could be boolean values. Unfortunately, we do not have any type information at this point. - // This means we are disabling support for boolean values in YAML that are expressed with these characters. - auto asText = v.as(); - if(asText.size() == 1 && asText.find_first_of("nyNYtfTF") == 0) // @TODO: should we disallow other strings too? - throw YAML::BadConversion(YAML::Mark()); // get's picked up by next catch block - - value_ = v.as(); + + // Placeholders for decode + bool asBool; + int64_t asInt; + double asDouble; + + // Text boolean values should be treated as a string + auto asString = v.as(); + bool isTextBool = asString.size() == 1 && asString.find_first_of("nyNYtfTF") == 0; + + if(YAML::convert::decode(v, asBool) && !isTextBool) { + value_ = asBool; type_ = NodeType::Bool; - } catch(const YAML::BadConversion& /*e*/) { - try { - value_ = v.as(); - type_ = NodeType::Int64; - } catch(const YAML::BadConversion& /*e*/) { - try { - value_ = v.as(); - type_ = NodeType::Float64; - } catch(const YAML::BadConversion& /*e*/) { - try { - value_ = v.as(); - type_ = NodeType::String; - } catch (const YAML::BadConversion& /*e*/) { - ABORT("Cannot convert YAML node {}", v); - } - } - } } - + else if(YAML::convert::decode(v, asInt)) { + value_ = asInt; + type_ = NodeType::Int64; + } + else if(YAML::convert::decode(v, asDouble)) { + value_ = asDouble; + type_ = NodeType::Float64; + } + else { + value_ = asString; + type_ = NodeType::String; + } + ABORT_IF(ph_, "ph_ should be undefined"); ABORT_IF(!array_.empty(), "array_ should be empty"); } @@ -223,7 +230,7 @@ class FastOpt { type_ = NodeType::Map; } - // Build a Map node, uses std::string as key, which gets hashed to size_t and used in the function above. + // Build a Map node, uses std::string as key, which gets hashed to uint64_t and used in the function above. void makeMap(const std::map& m) { std::map mi; for(const auto& it : m) { @@ -265,13 +272,14 @@ class FastOpt { public: // Constructor to recursively create a FastOpt object from a YAML::Node following the yaml structure. - FastOpt(const YAML::Node& node) + FastOpt(const YAML::Node& node) { construct(node); } - FastOpt(const YAML::Node& node, uint64_t fingerprint) + FastOpt(const YAML::Node& node, uint64_t fingerprint) : fingerprint_{fingerprint} { construct(node); } + // Predicates for node types bool isSequence() const { return type_ == NodeType::Sequence; } @@ -281,20 +289,20 @@ class FastOpt { } bool isScalar() const { - return type_ == NodeType::Bool - || type_ == NodeType::Float64 - || type_ == NodeType::Int64 + return type_ == NodeType::Bool + || type_ == NodeType::Float64 + || type_ == NodeType::Int64 || type_ == NodeType::String; } bool isNull() const { return type_ == NodeType::Null; - } + } bool isInt() const { return type_ == NodeType::Int64; - } - + } + bool isBool() const { return type_ == NodeType::Bool; } @@ -320,11 +328,11 @@ class FastOpt { std::swap(array_, other.array_); std::swap(type_, other.type_); std::swap(elements_, other.elements_); - // leave fingerprint alone as it needed by parent node. + // leave fingerprint alone as it needed by parent node. } - // Is the hashed key in a map? - bool has(size_t keyId) const { + // Is the hashed key in a map? + bool has(uint64_t keyId) const { if(isMap() && elements_ > 0) { const auto& ptr = phLookup(keyId); return ptr ? ptr->fingerprint_ == keyId : false; @@ -348,27 +356,27 @@ class FastOpt { } // access sequence or map element - const FastOpt& operator[](size_t keyId) const { + const FastOpt& operator[](uint64_t keyId) const { if(isSequence()) { - const auto& ptr = arrayLookup(keyId); + const auto& ptr = arrayLookup((size_t)keyId); ABORT_IF(!ptr, "Unseen key {}" , keyId); return *ptr; } else if(isMap()) { const auto& ptr = phLookup(keyId); ABORT_IF(!ptr || ptr->fingerprint_ != keyId, "Unseen key {}", keyId); - return *ptr; + return *ptr; } else { ABORT("Not a sequence or map node"); } } + // operator [] overloading for non-uint64_t keys const FastOpt& operator[](int key) const { - return operator[]((size_t)key); + return operator[]((uint64_t)key); } const FastOpt& operator[](const char* const key) const { - // MacOS requires explicit cast to size_t before we can use it. - return operator[]((size_t)crc::crc(key)); + return operator[](crc::crc(key)); } const FastOpt& operator[](const std::string& key) const { diff --git a/src/common/file_stream.cpp b/src/common/file_stream.cpp index 78cbb12fa..5b11d373a 100755 --- a/src/common/file_stream.cpp +++ b/src/common/file_stream.cpp @@ -23,7 +23,7 @@ InputFileStream::InputFileStream(const std::string &file) : std::istream(NULL) { // the special syntax "command |" starts command in a sh shell and reads out its result if (marian::utils::endsWith(file, "|")) { -#ifdef __unix__ +#if defined(__unix__) && !defined(WASM) auto command = file.substr(0, file.size() - 1); // open as a pipe pipe_ = popen(command.c_str(), "r"); @@ -45,8 +45,12 @@ InputFileStream::InputFileStream(const std::string &file) // insert .gz decompression if(marian::utils::endsWith(file, ".gz")) { +#if defined(WASM) + ABORT(".gz file decompression not supported in WASM builds of Marian: {}", file); +#else streamBuf2_ = std::move(streamBuf1_); streamBuf1_.reset(new zstr::istreambuf(streamBuf2_.get())); +#endif } // initialize the underlying istream @@ -94,8 +98,12 @@ OutputFileStream::OutputFileStream(const std::string &file) ABORT_IF(ret != streamBuf1_.get(), "Return value is not equal to streambuf pointer, that is weird"); if(file_.extension() == marian::filesystem::Path(".gz")) { +#if defined(WASM) + ABORT(".gz file decompression not supported in WASM builds of Marian: {}", file); +#else streamBuf2_.reset(new zstr::ostreambuf(streamBuf1_.get())); this->init(streamBuf2_.get()); +#endif } else { this->init(streamBuf1_.get()); } diff --git a/src/common/file_stream.h b/src/common/file_stream.h index ccf33ed86..9b9c76c17 100644 --- a/src/common/file_stream.h +++ b/src/common/file_stream.h @@ -26,7 +26,9 @@ #pragma warning(push) // 4101: 'identifier' : unreferenced local variable. One parameter variable in zstr.hpp is not used. #pragma warning(disable : 4101) #endif +#ifndef WASM #include "3rd_party/zstr/zstr.hpp" +#endif #ifdef _MSC_VER #pragma warning(pop) #endif diff --git a/src/common/logging.cpp b/src/common/logging.cpp index 62d76feea..c60ebbcae 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -145,7 +145,11 @@ void switchtoMultinodeLogging(std::string nodeIdStr) { namespace marian { std::string noinline getCallStack(size_t skipLevels) { + #ifdef WASM + return "Callstacks not supported in WASM builds currently"; + #else return ::Microsoft::MSR::CNTK::DebugUtil::GetCallStack(skipLevels + 2, /*makeFunctionNamesStandOut=*/true); + #endif } void noinline logCallStack(size_t skipLevels) { diff --git a/src/functional/operators.h b/src/functional/operators.h index 6345bfb61..827c9a30a 100755 --- a/src/functional/operators.h +++ b/src/functional/operators.h @@ -265,11 +265,11 @@ struct Ops { // @TODO: get rid of loop4 with proper intrisics static inline float32x4 sgn(const float32x4& x) { return loop4(Ops::sgn, x); } - +#ifndef WASM static inline float32x4 round(const float32x4& x) { return _mm_round_ps(x, _MM_FROUND_TO_NEAREST_INT); } static inline float32x4 floor(const float32x4& x) { return _mm_floor_ps(x); } static inline float32x4 ceil(const float32x4& x) { return _mm_ceil_ps(x); } - +#endif static inline float32x4 add(const float32x4& x, const float32x4& y) { return _mm_add_ps(x, y); } static inline float32x4 sub(const float32x4& x, const float32x4& y) { return _mm_sub_ps(x, y); } static inline float32x4 mul(const float32x4& x, const float32x4& y) { return _mm_mul_ps(x, y); } diff --git a/src/graph/node_initializers.cpp b/src/graph/node_initializers.cpp index 531cfaad0..c12d1498c 100755 --- a/src/graph/node_initializers.cpp +++ b/src/graph/node_initializers.cpp @@ -254,7 +254,7 @@ template Ptr range(IndexType begin, IndexType end, I } // namespace marian -#if BLAS_FOUND +#if BLAS_FOUND && !WASM #include "faiss/VectorTransform.h" namespace marian { diff --git a/src/layers/generic.cpp b/src/layers/generic.cpp index 64b4f4cbb..4e6b8ea21 100755 --- a/src/layers/generic.cpp +++ b/src/layers/generic.cpp @@ -221,9 +221,13 @@ namespace marian { // this option is only set in the decoder if(!lsh_ && options_->hasAndNotEmpty("output-approx-knn")) { +#ifdef WASM + ABORT("LSH is not supported in wasm builds of marian."); +#else auto k = opt>("output-approx-knn")[0]; auto nbits = opt>("output-approx-knn")[1]; lsh_ = New(k, nbits); +#endif } auto name = options_->get("prefix"); @@ -277,7 +281,11 @@ namespace marian { if(lsh_) { ABORT_IF( transA, "Transposed query not supported for LSH"); ABORT_IF(!transB, "Untransposed indexed matrix not supported for LSH"); +#ifdef WASM + ABORT("LSH is not supported in wasm builds of marian."); +#else return lsh_->apply(x, W, b); // knows how to deal with undefined bias +#endif } else { return affineOrDot(x, W, b, transA, transB); } diff --git a/src/layers/lsh.h b/src/layers/lsh.h index bf498cc60..625f027ad 100644 --- a/src/layers/lsh.h +++ b/src/layers/lsh.h @@ -18,7 +18,9 @@ class LSH { Expr apply(Expr query, Expr values, Expr bias); private: +#ifndef WASM Ptr index_; +#endif size_t indexHash_{0}; int k_{100}; diff --git a/src/rescorer/score_collector.h b/src/rescorer/score_collector.h index 278c64393..29fcfcc9d 100644 --- a/src/rescorer/score_collector.h +++ b/src/rescorer/score_collector.h @@ -35,11 +35,15 @@ class ScoreCollector { std::string getAlignment(const data::SoftAlignment& align); float getAlignmentThreshold(const std::string& str) { + #if WITHOUT_EXCEPTIONS + return str.size() == 0 ? 0.f : std::max(std::stof(str), 0.f); + #else try { return std::max(std::stof(str), 0.f); } catch(...) { return 0.f; } + #endif } }; diff --git a/src/tensors/cpu/prod.cpp b/src/tensors/cpu/prod.cpp index 781d75809..4d761cf4b 100755 --- a/src/tensors/cpu/prod.cpp +++ b/src/tensors/cpu/prod.cpp @@ -9,10 +9,12 @@ #if MKL_FOUND #include -#else -#if BLAS_FOUND -#include -#endif +#elif BLAS_FOUND + #if WASM_COMPATIBLE_BLAS + #include "3rd_party/onnxjs/src/wasm-ops/gemm.h" + #else + #include + #endif // WASM_COMPATIBLE_BLAS #endif #include "integer_common.h" @@ -195,85 +197,7 @@ void ProdBatched(marian::Tensor C, bool transB, float beta, float scalar) { - if (C->getBackend()->isLegacyBatchedGemm()) { ProdBatchedOld(C, allocator, A, B, transA, transB, beta, scalar); - } -#if MKL_FOUND - float alpha = scalar; - - size_t batchA = A->shape().elements() / (A->shape()[-1] * A->shape()[-2]); - size_t batchB = B->shape().elements() / (B->shape()[-1] * B->shape()[-2]); - - size_t m = A->shape()[-2]; - size_t k = A->shape()[-1]; - CBLAS_TRANSPOSE transA_forarr = CblasNoTrans; - if(transA) { - std::swap(m, k); - transA_forarr = CblasTrans; - } - - size_t l = B->shape()[-2]; - size_t n = B->shape()[-1]; - CBLAS_TRANSPOSE transB_forarr = CblasNoTrans; - if(transB) { - std::swap(l, n); - transB_forarr = CblasTrans; - } - - size_t lda = A->shape()[-1]; - size_t ldb = B->shape()[-1]; - size_t ldc = B->shape()[-1]; - - if(transB) - ldc = B->shape()[-2]; - - auto strideB = batchB == 1 ? 0 : n * k; - auto strideA = batchA == 1 ? 0 : m * k; - auto strideC = n * m; - auto batchC = std::max(batchA, batchB); - - static const constexpr size_t group_count = 1; - const std::vector transa_arr(group_count, transA_forarr); - const std::vector transb_arr(group_count, transB_forarr); - const std::vector m_arr(group_count, (MKL_INT)m); - const std::vector n_arr(group_count, (MKL_INT)n); - const std::vector k_arr(group_count, (MKL_INT)k); - const std::vector alpha_arr(group_count, alpha); - const std::vector beta_arr(group_count, beta); - const std::vector lda_arr(group_count, (MKL_INT)lda); - const std::vector ldb_arr(group_count, (MKL_INT)ldb); - const std::vector ldc_arr(group_count, (MKL_INT)ldc); - const std::vector group_size(group_count, (MKL_INT)batchC); - - std::vector a_array(batchC, nullptr); - std::vector b_array(batchC, nullptr); - std::vector c_array(batchC, nullptr); - for(size_t i = 0; i < batchC; ++i) { - a_array[i] = A->data() + (i % batchA) * strideA; - b_array[i] = B->data() + (i % batchB) * strideB; - c_array[i] = C->data() + i * strideC; - } - cblas_sgemm_batch (CblasRowMajor, - &transa_arr[0], - &transb_arr[0], - &m_arr[0], - &n_arr[0], - &k_arr[0], - &alpha_arr[0], - &a_array[0], - &lda_arr[0], - &b_array[0], - &ldb_arr[0], - &beta_arr[0], - &c_array[0], - &ldc_arr[0], - group_count, - &group_size[0]); - -#else - C; A; B; transA; transB; beta; scalar; - ABORT("You need to compile with MKL in order to use the CPU version"); -#endif } void ProdWithBias(marian::Tensor C, diff --git a/src/tensors/cpu/prod_blas.h b/src/tensors/cpu/prod_blas.h index a591fdd26..1d6757927 100644 --- a/src/tensors/cpu/prod_blas.h +++ b/src/tensors/cpu/prod_blas.h @@ -1,9 +1,11 @@ #if MKL_FOUND #include -#else -#if BLAS_FOUND -#include -#endif +#elif BLAS_FOUND + #if WASM_COMPATIBLE_BLAS + #include "3rd_party/onnxjs/src/wasm-ops/gemm.h" + #else + #include + #endif // WASM_COMPATIBLE_BLAS #endif inline void sgemm(bool transA, @@ -20,20 +22,24 @@ inline void sgemm(bool transA, float* c, int ldc) { #if BLAS_FOUND - cblas_sgemm(CblasRowMajor, - transA ? CblasTrans : CblasNoTrans, - transB ? CblasTrans : CblasNoTrans, - rows_a, - rows_b, - width, - alpha, - a, - lda, - b, - ldb, - beta, - c, - ldc); + #if WASM_COMPATIBLE_BLAS + gemm_f32_imp(transA, transB, rows_a, rows_b, width, alpha, a, b, beta, c); + #else + cblas_sgemm(CblasRowMajor, + transA ? CblasTrans : CblasNoTrans, + transB ? CblasTrans : CblasNoTrans, + rows_a, + rows_b, + width, + alpha, + a, + lda, + b, + ldb, + beta, + c, + ldc); + #endif #else transA; transB; rows_a; rows_b; width; alpha; a; lda; b; ldb; beta; c; ldc; // make compiler happy ABORT("Marian must be compiled with a BLAS library"); diff --git a/src/translator/output_printer.h b/src/translator/output_printer.h index 603eedba5..69fd40446 100755 --- a/src/translator/output_printer.h +++ b/src/translator/output_printer.h @@ -99,11 +99,15 @@ class OutputPrinter { std::string getWordScores(const Hypothesis::PtrType& hyp); float getAlignmentThreshold(const std::string& str) { +#if WITHOUT_EXCEPTIONS + return str.size() == 0 ? 0.f : std::max(std::stof(str), 0.f); +#else try { return std::max(std::stof(str), 0.f); } catch(...) { return 0.f; } +#endif // WITHOUT_EXCEPTIONS } }; } // namespace marian diff --git a/src/translator/translator.h b/src/translator/translator.h index 8ba5a2fb2..9fee0a81d 100755 --- a/src/translator/translator.h +++ b/src/translator/translator.h @@ -7,7 +7,9 @@ #include "data/shortlist.h" #include "data/text_input.h" +#if USE_PTHREADS #include "3rd_party/threadpool.h" +#endif #include "translator/history.h" #include "translator/output_collector.h" @@ -74,7 +76,9 @@ class Translate : public ModelTask { auto devices = Config::getDevices(options_); numDevices_ = devices.size(); +#if USE_PTHREADS ThreadPool threadPool(numDevices_, numDevices_); +#endif scorers_.resize(numDevices_); graphs_.resize(numDevices_); @@ -114,7 +118,11 @@ class Translate : public ModelTask { graph->forward(); }; +#if USE_PTHREADS threadPool.enqueue(task, device, id++); +#else + task(device, id++); +#endif } if(options_->get("output-sampling", false)) { @@ -130,9 +138,16 @@ class Translate : public ModelTask { } void run() override { + #if USE_PTHREADS data::BatchGenerator bg(corpus_, options_); + #else + // Set to false to run non-async mode + data::BatchGenerator bg(corpus_, options_, nullptr, false); + #endif +#if USE_PTHREADS ThreadPool threadPool(numDevices_, numDevices_); +#endif size_t batchId = 0; auto collector = New(options_->get("output")); @@ -178,7 +193,11 @@ class Translate : public ModelTask { } }; +#if USE_PTHREADS threadPool.enqueue(task, batchId++); +#else + task(batchId++); +#endif } } @@ -262,7 +281,12 @@ class TranslateService : public ModelServiceTask { ? convertTsvToLists(input, options_->get("tsv-fields", 1)) : std::vector({input}); auto corpus_ = New(inputs, srcVocabs_, options_); + #if USE_PTHREADS data::BatchGenerator batchGenerator(corpus_, options_); + #else + // Set to false to run non-async mode + data::BatchGenerator batchGenerator(corpus_, options_, nullptr, false); + #endif auto collector = New(options_->get("quiet-translation", false)); auto printer = New(options_, trgVocab_); @@ -271,7 +295,9 @@ class TranslateService : public ModelServiceTask { batchGenerator.prepare(); { +#if USE_PTHREADS ThreadPool threadPool_(numDevices_, numDevices_); +#endif for(auto batch : batchGenerator) { auto task = [=](size_t id) { @@ -294,7 +320,11 @@ class TranslateService : public ModelServiceTask { } }; +#if USE_PTHREADS threadPool_.enqueue(task, batchId); +#else + task(batchId); +#endif batchId++; } } diff --git a/wasm/Dockerfile b/wasm/Dockerfile new file mode 100644 index 000000000..b16e20a3e --- /dev/null +++ b/wasm/Dockerfile @@ -0,0 +1,15 @@ +FROM emscripten/emsdk:2.0.9 + +# Install specific version of CMake +WORKDIR /usr +RUN wget https://github.com/Kitware/CMake/releases/download/v3.17.2/cmake-3.17.2-Linux-x86_64.tar.gz -qO-\ + | tar xzf - --strip-components 1 + +# Install Python and Java (needed for Closure Compiler minification) +RUN apt-get update \ + && apt-get install -y \ + python3 \ + default-jre + +# Necessary for benchmarking +RUN pip3 install sacrebleu diff --git a/wasm/Makefile b/wasm/Makefile new file mode 100644 index 000000000..94970920b --- /dev/null +++ b/wasm/Makefile @@ -0,0 +1,82 @@ +# -*- mode: makefile-gmake; indent-tabs-mode: true; tab-width: 4 -*- +SHELL = bash +PWD = $(shell pwd) +WASM_IMAGE = local/marian-wasm-docker + +# Build the Docker image for WASM builds +wasm-image: + docker build -t local/marian-wasm-docker . + +# Commands for native compilation: +cmake_native_decoder_cmd = cmake --debug-output -Wno-dev +cmake_native_decoder_cmd += -DUSE_STATIC_LIBS=on +cmake_native_decoder_cmd += -DUSE_SENTENCEPIECE=on +cmake_native_decoder_cmd += -DCOMPILE_CUDA=off +cmake_native_decoder_cmd += -DUSE_DOXYGEN=off +cmake_native_decoder_cmd += -DUSE_FBGEMM=off +cmake_native_decoder_cmd += -DCOMPILE_LIBRARY_ONLY=off +cmake_native_decoder_cmd += -DUSE_MKL=off +cmake_native_decoder_cmd += -DCOMPILE_CPU=on + +# Commands for wasm compilation: +cmake_wasm_decoder_cmd = ${cmake_native_decoder_cmd} +cmake_wasm_decoder_cmd += -DCOMPILE_WASM=on + +# Commands for running things on Docker +docker_mounts = ${PWD}/..:/repo +docker_mounts += ${HOME}/.ccache:/.ccache +run_on_docker = docker run --rm +run_on_docker += $(addprefix -v, ${docker_mounts}) +run_on_docker += ${INTERACTIVE_DOCKER_SESSION} + +${HOME}/.ccache: + mkdir -p $@ + +############## Compiling wasm marian-decoder on Docker ################# +# Remove the WASM build dir, forcing a clean compilation attempt +clean-wasm-docker: BUILD_DIR = /repo/build-wasm-docker +clean-wasm-docker: ${HOME}/.ccache + ${run_on_docker} ${WASM_IMAGE} bash -c '(rm -rf ${BUILD_DIR} || true)' + +# Compile marian-decoder to WASM +compile-wasm-docker: BUILD_DIR = /repo/build-wasm-docker +compile-wasm-docker: ${HOME}/.ccache + ${run_on_docker} ${WASM_IMAGE} bash -c 'mkdir -p ${BUILD_DIR} && \ +cd ${BUILD_DIR} && \ +(emcmake ${cmake_wasm_decoder_cmd} .. && \ +(emmake make -j4))' + +# Prepare files to be used with WASM marian-decoder +package-files-wasm-docker: BUILD_DIR = /repo/build-wasm-docker +package-files-wasm-docker: MODELS_DIR = /repo/models +package-files-wasm-docker: + ${run_on_docker} ${WASM_IMAGE} bash -c 'sacrebleu -t wmt13 -l es-en --echo src > /repo/models/newstest2013.es && \ +head -n300 /repo/models/newstest2013.es > /repo/models/newstest2013.es.top300lines && \ +python3 /emsdk/upstream/emscripten/tools/file_packager.py ${BUILD_DIR}/model-files.data --preload ${MODELS_DIR}/@ --js-output=${BUILD_DIR}/model-files.js' + +# Run WASM marian-decoder +run-wasm-docker: BUILD_DIR = /repo/build-wasm-docker +run-wasm-docker: + ${run_on_docker} -p 8000:8000 ${WASM_IMAGE} bash -c 'emrun --no_browser --port 8000 ${BUILD_DIR}' + +wasm-shell: INTERACTIVE_DOCKER_SESSION = -it +wasm-shell: + ${run_on_docker} ${WASM_IMAGE} bash + + +# Compile stdin test directly +cmake_compile_stdin_test_cmd = em++ +cmake_compile_stdin_test_cmd += -O2 -s WASM=1 -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0 +cmake_compile_stdin_test_cmd += -s FORCE_FILESYSTEM=1 +cmake_compile_stdin_test_cmd += -s ALLOW_MEMORY_GROWTH=1 +cmake_compile_stdin_test_cmd += -o test-stdin-wasm.html +cmake_compile_stdin_test_cmd += --pre-js /repo/wasm/pre-module.js --post-js /repo/wasm/post-module.js --shell-file /repo/wasm/custom_shell.html +cmake_compile_stdin_test_cmd += /repo/wasm/test_stdin.cpp + +compile-and-run-stdin-test-wasm: BUILD_DIR = /repo/build-test-stdin-wasm +compile-and-run-stdin-test-wasm: + ${run_on_docker} ${WASM_IMAGE} bash -c '(rm -rf ${BUILD_DIR} || true) && \ +mkdir -p ${BUILD_DIR} &&\ +cd ${BUILD_DIR} &&\ +${cmake_compile_stdin_test_cmd} &&\ +${run_on_docker} -p 8009:8009 ${WASM_IMAGE} bash -c 'emrun --no_browser --port 8009 ${BUILD_DIR}' diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 000000000..fbafaaed1 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,141 @@ + +## WASM + +### Compiling wasm marian-decoder on local machine + +For docker based builds, please refer [Docker Compilation Steps](#Docker-Compilation) + +1. Download and Install Emscripten using following instructions (skip this step if emsdk tool chain is already installed) + * Get the latest sdk: `git clone https://github.com/emscripten-core/emsdk.git` + * Enter the cloned directory: `cd emsdk` + * Install the lastest sdk tools: `./emsdk install latest` + * Activate the latest sdk tools: `./emsdk activate latest` + * Activate path variables: `source ./emsdk_env.sh` + + `EMSDK` environment variable will point to the valid emsdk repo after executing the instructions above. + +2. Compile + + * Create a build directory (e.g. `build-wasm`) and run compilation commands inside it as below: + ```bash + mkdir build-wasm; cd build-wasm + + emcmake cmake -DCOMPILE_CUDA=off -DUSE_STATIC_LIBS=on -DUSE_DOXYGEN=off -DUSE_FBGEMM=off -DUSE_MKL=off -DUSE_NCCL=off -DCOMPILE_WASM=on ../ + + emmake make -j + ``` + + The artifacts (.js and .wasm files) will be available in `build-wasm` folder. + +### Performing Translation using wasm marian-decoder + +1. Pre-processing (Package files to WASM-compiled runtime) + + This step is required to be able to perform translation using wasm binary. + + The script `package-benchmark.sh` inside `wasm` folder downloads and packages the Bergamot project specific Spanish to English translation models, vocabulary, lexical shortlist files and a News test file as source text for translation. (Please install `sacrebleu` before if not installed already using command: `pip install sacrebleu`). + + From the build directory (`build-wasm` for local builds or `build-wasm-docker` for docker builds), run: + + ```bash + bash ../wasm/package-benchmark.sh + ``` + +2. Perform Translation + 1. Launch the emscripten-generated HTML page in a web browser using following commands: + ```bash + emrun --no_browser --port 8000 . + ``` + + 2. Open up following link in Firefox nightly browser (replace `stdin-input` with the text that you want to translate and `command-line-args` with the appropriate model, vocabulary files etc.) + + ```bash + http://localhost:8000/marian-decoder.html?stdinInput=&arguments= + ``` + + e.g. To translate "Hola mundo" to English using the Bergamot project specific Spanish to English files (packaged above), open this link: + + ```bash + http://localhost:8000/marian-decoder.html?stdinInput=Hola mundo&arguments=-m /model.npz -v /vocab.esen.spm /vocab.esen.spm --cpu-threads 1 + ``` + + Note: To run in Chrome, launch Chrome with ` --js-flags="--experimental-wasm-simd"`, eg: + + ```bash + /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --js-flags="--experimental-wasm-simd" + ``` + + Please remember that the Developer Tools must not be open when opening the links or refreshing the page to run the benchmark again. + + 3. Open the Developer Tools and you should see the result in console. + + +### Benchmarking + +If you used the script `package-benchmark.sh` mentioned above then open following for benchmarking (please remember that the Developer Tools must not be open when opening the links or refreshing the page to run the benchmark again.) + +1. float32 + + `http://localhost:8000/marian-decoder.html?arguments=-m /model.npz -v /vocab.esen.spm /vocab.esen.spm -i /newstest2013.es.top300lines --beam-size 1 --mini-batch 32 --maxi-batch 100 --maxi-batch-sort src -w 128 --skip-cost --shortlist /lex.s2t 50 50 --cpu-threads 1` + +2. intgemm8 + + `http://localhost:8000/marian-decoder.html?arguments=-m /model.npz -v /vocab.esen.spm /vocab.esen.spm -i /newstest2013.es.top300lines --beam-size 1 --mini-batch 32 --maxi-batch 100 --maxi-batch-sort src -w 128 --skip-cost --shortlist /lex.s2t 50 50 --cpu-threads 1 --int8shift` + +3. intgemm8 with binary model file + + `http://localhost:8000/marian-decoder.html?arguments=-m /model.intgemm.bin -v /vocab.esen.spm /vocab.esen.spm -i /newstest2013.es.top300lines --beam-size 1 --mini-batch 32 --maxi-batch 100 --maxi-batch-sort src -w 128 --skip-cost --shortlist /lex.s2t 50 50 --cpu-threads 1 --int8shift` + +4. intgemm8alphas + + `http://localhost:8000/marian-decoder.html?arguments=-m /model.npz -v /vocab.esen.spm /vocab.esen.spm -i /newstest2013.es.top300lines --beam-size 1 --mini-batch 32 --maxi-batch 100 --maxi-batch-sort src -w 128 --skip-cost --shortlist /lex.s2t 50 50 --cpu-threads 1 --int8shiftAlphaAll` + +5. intgemm8alphas with binary model file + + `http://localhost:8000/marian-decoder.html?arguments=-m /model.intgemm.alphas.bin -v /vocab.esen.spm /vocab.esen.spm -i /newstest2013.es.top300lines --beam-size 1 --mini-batch 32 --maxi-batch 100 --maxi-batch-sort src -w 128 --skip-cost --shortlist /lex.s2t 50 50 --cpu-threads 1 --int8shiftAlphaAll` + +Note that intgemm options are only available in Firefox Nightly verified by visiting [this link](https://axis-of-eval.org/sandbox/wormhole-test.html). + + +### Compiling wasm marian-decoder on Docker + +Alternatively, wasm marian-decoder can also be compiled on docker. + +1. Prepare docker image for WASM compilation + This step is required only for the first time (or after any changes to docker image) + + ```bash + make wasm-image + ``` + +2. Compile to wasm + + ```bash + make compile-wasm-docker + ``` + The artifacts (.js and .wasm files) will be available in `build-wasm-docker` folder in root of this repository. + +3. Performing Translation + + Please follow [Performing Translation](#Perform-Translation) for this. + + +#### Additional helpful instructions for docker based builds + +1. Clean build (to start next compilation from scratch) + + ```bash + make clean-wasm-docker + ``` +2. Compile and run a wasm stdin test: + + ```bash + make compile-and-run-stdin-test-wasm + open "http://localhost:8009/compile-test-stdin-wasm.html" + ``` + +3. Enter a docker container shell for manually running commands: + + ```bash + make wasm-shell + ``` diff --git a/wasm/custom_shell.html b/wasm/custom_shell.html new file mode 100644 index 000000000..15d50e7ad --- /dev/null +++ b/wasm/custom_shell.html @@ -0,0 +1,479 @@ + + + + + + Emscripten-Generated Code + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Downloading...
+Resize canvasLock/hide mouse pointer     +
+ +
+
+ +
+ + + +{{{ SCRIPT }}} + + \ No newline at end of file diff --git a/wasm/package-benchmark.sh b/wasm/package-benchmark.sh new file mode 100644 index 000000000..e16ee08af --- /dev/null +++ b/wasm/package-benchmark.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +if [[ ! -e ../models ]]; then + mkdir -p ../models + if [[ ! -e ../students ]]; then + echo "Cloning https://github.com/browsermt/students)" + git clone --depth 1 --branch main --single-branch https://github.com/browsermt/students ../ + fi + + echo "Downloading files" + ../students/esen/download-models.sh + + echo "Copying downloaded files to models folder" + cp ../students/esen/esen.student.tiny11/vocab.esen* ../students/esen/esen.student.tiny11/model* ../students/esen/esen.student.tiny11/lex.s2t* ../models/ + sacrebleu -t wmt13 -l es-en --echo src > ../models/newstest2013.es + head -n300 ../models/newstest2013.es > ../models/newstest2013.es.top300lines + gunzip ../models/* +else + echo "models directory already exists in root folder; Using it to package files without downloading anything" +fi + +echo "Packaging files for wasm binary" +$EMSDK_PYTHON $EMSDK/upstream/emscripten/tools/file_packager.py model-files.data --preload ../models/@ --js-output=model-files.js + +echo "Enabling wormhole via APIs that compile and instantiate wasm module" +sed -i.bak 's/var result = WebAssembly.instantiateStreaming(response, info);/var result = WebAssembly.instantiateStreaming(response, info, {simdWormhole:true});/g' marian-decoder.js +sed -i.bak 's/return WebAssembly.instantiate(binary, info);/return WebAssembly.instantiate(binary, info, {simdWormhole:true});/g' marian-decoder.js +sed -i.bak 's/var module = new WebAssembly.Module(bytes);/var module = new WebAssembly.Module(bytes, {simdWormhole:true});/g' marian-decoder.js +echo "SUCCESS" diff --git a/wasm/post-module.js b/wasm/post-module.js new file mode 100644 index 000000000..e69de29bb diff --git a/wasm/pre-module.js b/wasm/pre-module.js new file mode 100644 index 000000000..11bbe7b2e --- /dev/null +++ b/wasm/pre-module.js @@ -0,0 +1,71 @@ +// Enables setting runtime args via the query string (as long as this is run in the main browser thread and not in a worker) +let stdinInput = false; +if ( + typeof window !== "undefined" && + window.location && + window.location.search +) { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get("stdinInput")) { + stdinInput = urlParams.get("stdinInput"); + console.log("Using stdinInput from URL"); + } + if (urlParams.get("arguments")) { + Module["arguments"] = urlParams.get("arguments").split(' ') + // Module["arguments"] = urlParams.get("arguments").split('%20'); + console.log("Using arguments from URL"); + } +} +console.log('stdinInput', stdinInput); +console.log('Module["arguments"]', Module["arguments"]); +Module["noInitialRun"] = true; +Module["onRuntimeInitialized"] = _ => { + try { + console.log("Calling main in a try-catch block to be able to get readable exception messages"); + callMain(Module["arguments"]) + } catch (exception) { + console.error("WASM exception thrown", Module.getExceptionMessage(exception)) + } +}; +var initStdInOutErr = function() { + var i = 0; + function stdin() { + if (stdinInput === false) { + console.log("STDIN: No stdin input specified"); + return null; + } + var input = stdinInput + "\n"; + if (i < input.length) { + var code = input.charCodeAt(i); + ++i; + console.log("STDIN: Feeding character code to stdin: ", code); + return code; + } else { + console.log("STDIN: Done feeding input via stdin: ", input); + return null; + } + } + + var stdoutBuffer = ""; + function stdout(code) { + if (code === "\n".charCodeAt(0) && stdoutBuffer !== "") { + console.log("STDOUT: ", stdoutBuffer); + stdoutBuffer = ""; + } else { + stdoutBuffer += String.fromCharCode(code); + } + } + + var stderrBuffer = ""; + function stderr(code) { + if (code === "\n".charCodeAt(0) && stderrBuffer !== "") { + console.log("STDERR: ", stderrBuffer); + stderrBuffer = ""; + } else { + stderrBuffer += String.fromCharCode(code); + } + } + + FS.init(stdin, stdout, stderr); +} +Module["preRun"].push(initStdInOutErr); diff --git a/wasm/test_stdin.cpp b/wasm/test_stdin.cpp new file mode 100644 index 000000000..b95673cbf --- /dev/null +++ b/wasm/test_stdin.cpp @@ -0,0 +1,65 @@ +/* + * Copyright 2013 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#include +#include +#include +#include +#include +#ifdef __EMSCRIPTEN__ +#include +#endif + +int line = 0; + +void main_loop() +{ + char str[10] = {0}; + int ret; + + errno = 0; + while (errno != EAGAIN) { + if (line == 0) { + ret = fgetc(stdin); + if (ret != EOF) putc(ret, stdout); + if (ret == '\n') line++; + } else if (line > 0) { + ret = scanf("%10s", str); + if (ret > 0) puts(str); + } + + int err = ferror(stdin); + if (ferror(stdin) && errno != EAGAIN) { + printf("error %d\n", err); + exit(EXIT_FAILURE); + } + + if (feof(stdin)) { + puts("eof"); + exit(EXIT_SUCCESS); + } + + clearerr(stdin); + } +} + +int main(int argc, char const *argv[]) +{ + fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); + + // SM shell doesn't implement an event loop and therefor doesn't support + // emscripten_set_main_loop. However, its stdin reads are sync so it + // should exit out after calling main_loop once. + main_loop(); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 60, 0); +#else + while (1) main_loop(); sleep(1); +#endif + return 0; +}