| //===-- lib/runtime/io-api-server.cpp ---------------------------*- C++ -*-===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| // Implements the RPC server-side handlling of the I/O statement API needed for | |
| // basic list-directed output (PRINT *) of intrinsic types for the GPU. | |
| #include "array.h" | |
| #include "io-api-gpu.h" | |
| #include "flang-rt/runtime/memory.h" | |
| #include "flang-rt/runtime/terminator.h" | |
| #include "flang/Runtime/io-api.h" | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <tuple> | |
| #include <shared/rpc.h> | |
| #include <shared/rpc_dispatch.h> | |
| namespace Fortran::runtime::io { | |
| namespace { | |
| // Context used to chain the IO operations once run. | |
| struct IOContext { | |
| Cookie cookie = nullptr; | |
| enum Iostat result = IostatOk; | |
| }; | |
| // The base class to store deferred execution of a function. Uses function | |
| // pointers for type erasure to avoid virtual dispatch. | |
| struct DeferredFunctionBase { | |
| using ExecuteFn = void (*)(void *, IOContext &); | |
| using DestroyFn = void (*)(void *); | |
| DeferredFunctionBase(void *impl, ExecuteFn exec, DestroyFn dtor) | |
| : impl_(impl), execute_(exec), destroy_(dtor) {} | |
| DeferredFunctionBase(const DeferredFunctionBase &) = delete; | |
| DeferredFunctionBase &operator=(const DeferredFunctionBase &) = delete; | |
| DeferredFunctionBase(DeferredFunctionBase &&other) | |
| : impl_(other.impl_), execute_(other.execute_), destroy_(other.destroy_) { | |
| other.impl_ = nullptr; | |
| } | |
| DeferredFunctionBase &operator=(DeferredFunctionBase &&other) { | |
| if (this != &other) { | |
| reset(); | |
| impl_ = other.impl_; | |
| execute_ = other.execute_; | |
| destroy_ = other.destroy_; | |
| other.impl_ = nullptr; | |
| } | |
| return *this; | |
| } | |
| ~DeferredFunctionBase() { reset(); } | |
| void execute(IOContext &ctx) { execute_(impl_, ctx); } | |
| static OwningPtr<char> TempString(const char *str, std::size_t size) { | |
| if (!str) { | |
| return {}; | |
| } | |
| OwningPtr<char> temp = SizedNew<char>{Terminator{__FILE__, __LINE__}}(size); | |
| std::memcpy(temp.get(), str, size); | |
| return OwningPtr<char>(temp.release()); | |
| } | |
| static OwningPtr<char> TempString(const char *str) { | |
| if (!str) { | |
| return {}; | |
| } | |
| return TempString(str, std::strlen(str) + 1); | |
| } | |
| private: | |
| void reset() { | |
| if (impl_) { | |
| destroy_(impl_); | |
| FreeMemory(impl_); | |
| impl_ = nullptr; | |
| } | |
| } | |
| void *impl_ = nullptr; | |
| ExecuteFn execute_ = nullptr; | |
| DestroyFn destroy_ = nullptr; | |
| }; | |
| // Fortran does not support nested or recursive I/O, which is problematic for | |
| // parallel execution on a GPU. To support this, we defer execution of runtime | |
| // functions coming from the GPU's client until the end of that sequence is | |
| // reached. This allows us to finish them in a single pass. | |
| template <typename FnTy, typename... Args> struct DeferredFunction { | |
| FnTy fn_; | |
| std::tuple<std::decay_t<Args>...> args_; | |
| DeferredFunction(FnTy &&fn, Args &&...args) | |
| : fn_(std::forward<FnTy>(fn)), args_(std::forward<Args>(args)...) {} | |
| // When executing the final command queue we need to replace the temporary | |
| // values obtained from the GPU with the returned values from the actual | |
| // runtime functions. | |
| void execute(IOContext &ctx) { | |
| auto caller = [&](auto &&...args) { return fn_(Rewrite(args, ctx)...); }; | |
| using RetTy = std::invoke_result_t<FnTy, | |
| decltype(Rewrite(std::declval<Args &>(), ctx))...>; | |
| if constexpr (std::is_same_v<RetTy, Cookie>) { | |
| ctx.cookie = std::apply(caller, args_); | |
| } else if constexpr (std::is_same_v<RetTy, Iostat>) { | |
| ctx.result = std::apply(caller, args_); | |
| } else { | |
| std::apply(caller, args_); | |
| } | |
| } | |
| private: | |
| template <typename T> T &Rewrite(T &v, IOContext &) { return v; } | |
| const char *Rewrite(OwningPtr<char> &p, IOContext &) { return p.get(); } | |
| Cookie Rewrite(Cookie, IOContext &ctx) { return ctx.cookie; } | |
| }; | |
| template <typename Fn, typename... Args> | |
| DeferredFunctionBase MakeDeferred(Fn &&fn, Args &&...args) { | |
| Terminator terminator{__FILE__, __LINE__}; | |
| using Ty = DeferredFunction<Fn, Args...>; | |
| auto ptr = SizedNew<Ty>{terminator}( | |
| sizeof(Ty), std::forward<Fn>(fn), std::forward<Args>(args)...); | |
| void *raw = ptr.release(); | |
| return DeferredFunctionBase( | |
| raw, | |
| [](void *self, IOContext &ctx) { static_cast<Ty *>(self)->execute(ctx); }, | |
| [](void *self) { static_cast<Ty *>(self)->~Ty(); }); | |
| } | |
| // The context associated with the queue of deferred functions. This serves as | |
| // our cookie object while executing this on the GPU. | |
| struct DeferredContext { | |
| IOContext ioCtx; | |
| DynamicArray<DeferredFunctionBase> commands; | |
| }; | |
| template <typename FnTy, typename... Args> | |
| bool EnqueueDeferred(FnTy &&fn, Cookie cookie, Args &&...args) { | |
| DeferredContext *ctx = reinterpret_cast<DeferredContext *>(cookie); | |
| ctx->commands.emplace_back( | |
| MakeDeferred(fn, cookie, std::forward<Args>(args)...)); | |
| return true; | |
| } | |
| template <std::uint32_t NumLanes> | |
| rpc::RPCStatus HandleOpcodesImpl(rpc::Server::Port &port) { | |
| switch (port.get_opcode()) { | |
| case BeginExternalListOutput_Opcode: | |
| rpc::invoke<NumLanes>(port, | |
| [](ExternalUnit unitNumber, const char *sourceFile, | |
| int sourceLine) -> Cookie { | |
| DeferredContext *ctx = new (AllocateMemoryOrCrash( | |
| Terminator{__FILE__, __LINE__}, sizeof(DeferredContext))) | |
| DeferredContext; | |
| ctx->commands.emplace_back( | |
| MakeDeferred(IONAME(BeginExternalListOutput), unitNumber, | |
| DeferredFunctionBase::TempString(sourceFile), sourceLine)); | |
| return reinterpret_cast<Cookie>(ctx); | |
| }); | |
| break; | |
| case BeginExternalFormattedOutput_Opcode: | |
| rpc::invoke<NumLanes>(port, | |
| [](const char *format, std::size_t formatLength, | |
| const Descriptor *formatDescriptor, ExternalUnit unitNumber, | |
| const char *sourceFile, int sourceLine) -> Cookie { | |
| Terminator terminator{__FILE__, __LINE__}; | |
| if (formatDescriptor) | |
| terminator.Crash("Non-trivial format descriptors are unsupported"); | |
| DeferredContext *ctx = | |
| new (AllocateMemoryOrCrash(terminator, sizeof(DeferredContext))) | |
| DeferredContext; | |
| ctx->commands.emplace_back( | |
| MakeDeferred(IONAME(BeginExternalFormattedOutput), | |
| DeferredFunctionBase::TempString(format, formatLength), | |
| formatLength, formatDescriptor, unitNumber, | |
| DeferredFunctionBase::TempString(sourceFile), sourceLine)); | |
| return reinterpret_cast<Cookie>(ctx); | |
| }); | |
| break; | |
| case EnableHandlers_Opcode: | |
| rpc::invoke<NumLanes>(port, | |
| [](Cookie cookie, bool hasIoStat, bool hasErr, bool hasEnd, bool hasEor, | |
| bool hasIoMsg) -> void { | |
| EnqueueDeferred(IONAME(EnableHandlers), cookie, hasIoStat, hasErr, | |
| hasEnd, hasEor, hasIoMsg); | |
| }); | |
| break; | |
| case EndIoStatement_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie) -> Iostat { | |
| DeferredContext *ctx = reinterpret_cast<DeferredContext *>(cookie); | |
| ctx->commands.emplace_back(MakeDeferred(IONAME(EndIoStatement), cookie)); | |
| for (auto &fn : ctx->commands) { | |
| fn.execute(ctx->ioCtx); | |
| } | |
| Iostat result = ctx->ioCtx.result; | |
| ctx->~DeferredContext(); | |
| FreeMemory(ctx); | |
| return result; | |
| }); | |
| break; | |
| case OutputInteger8_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, std::int8_t n) -> bool { | |
| return EnqueueDeferred(IONAME(OutputInteger8), cookie, n); | |
| }); | |
| break; | |
| case OutputInteger16_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, std::int16_t n) -> bool { | |
| return EnqueueDeferred(IONAME(OutputInteger16), cookie, n); | |
| }); | |
| break; | |
| case OutputInteger32_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, std::int32_t n) -> bool { | |
| return EnqueueDeferred(IONAME(OutputInteger32), cookie, n); | |
| }); | |
| break; | |
| case OutputInteger64_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, std::int64_t n) -> bool { | |
| return EnqueueDeferred(IONAME(OutputInteger64), cookie, n); | |
| }); | |
| break; | |
| #ifdef __SIZEOF_INT128__ | |
| case OutputInteger128_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, common::int128_t n) -> bool { | |
| return EnqueueDeferred(IONAME(OutputInteger128), cookie, n); | |
| }); | |
| break; | |
| #endif | |
| case OutputReal32_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, float x) -> bool { | |
| return EnqueueDeferred(IONAME(OutputReal32), cookie, x); | |
| }); | |
| break; | |
| case OutputReal64_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, double x) -> bool { | |
| return EnqueueDeferred(IONAME(OutputReal64), cookie, x); | |
| }); | |
| break; | |
| case OutputComplex32_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, float re, float im) -> bool { | |
| return EnqueueDeferred(IONAME(OutputComplex32), cookie, re, im); | |
| }); | |
| break; | |
| case OutputComplex64_Opcode: | |
| rpc::invoke<NumLanes>( | |
| port, [](Cookie cookie, double re, double im) -> bool { | |
| return EnqueueDeferred(IONAME(OutputComplex64), cookie, re, im); | |
| }); | |
| break; | |
| case OutputAscii_Opcode: | |
| rpc::invoke<NumLanes>( | |
| port, [](Cookie cookie, const char *x, std::size_t length) -> bool { | |
| return EnqueueDeferred(IONAME(OutputAscii), cookie, | |
| DeferredFunctionBase::TempString(x, length), length); | |
| }); | |
| break; | |
| case OutputCharacter_Opcode: | |
| rpc::invoke<NumLanes>(port, | |
| [](Cookie cookie, const char *x, std::size_t length, int kind) -> bool { | |
| return EnqueueDeferred(IONAME(OutputCharacter), cookie, | |
| DeferredFunctionBase::TempString(x, length * kind), length, kind); | |
| }); | |
| break; | |
| case OutputLogical_Opcode: | |
| rpc::invoke<NumLanes>(port, [](Cookie cookie, bool truth) -> bool { | |
| return EnqueueDeferred(IONAME(OutputLogical), cookie, truth); | |
| }); | |
| break; | |
| default: | |
| return rpc::RPC_UNHANDLED_OPCODE; | |
| } | |
| return rpc::RPC_SUCCESS; | |
| } | |
| } // anonymous namespace | |
| using RPCCallbackTy = std::uint32_t (*)(void *, std::uint32_t); | |
| // COFF workaround for the lack of weak symbols, copied from the sanitizers. | |
| #if defined(_MSC_VER) | |
| extern "C" void register_rpc_callback_stub(RPCCallbackTy) { | |
| // Stub function to be replaced with __tgt_register_rpc_callback if present. | |
| } | |
| #pragma comment(linker, \ | |
| "/alternatename:__tgt_register_rpc_callback=" \ | |
| "register_rpc_callback_stub") | |
| #if defined(_M_ARM64EC) | |
| #pragma comment(linker, \ | |
| "/alternatename:#__tgt_register_rpc_callback=" \ | |
| "#register_rpc_callback_stub") | |
| #endif | |
| #endif | |
| // Used for I/O from the offloading device runtime. | |
| static std::uint32_t HandleRPCOpcodes(void *raw, std::uint32_t numLanes) { | |
| rpc::Server::Port &port = *reinterpret_cast<rpc::Server::Port *>(raw); | |
| switch (numLanes) { | |
| case 1: | |
| return HandleOpcodesImpl<1>(port); | |
| case 32: | |
| return HandleOpcodesImpl<32>(port); | |
| case 64: | |
| return HandleOpcodesImpl<64>(port); | |
| default: | |
| return rpc::RPC_ERROR; | |
| } | |
| } | |
| #if defined(__ELF__) | |
| extern "C" [[gnu::weak]] void __tgt_register_rpc_callback(RPCCallbackTy); | |
| #elif defined(__APPLE__) | |
| extern "C" [[gnu::weak]] | |
| void __tgt_register_rpc_callback(RPCCallbackTy) {} | |
| #else | |
| extern "C" void __tgt_register_rpc_callback(RPCCallbackTy); | |
| #endif | |
| void RegisterRPCHandlers() { | |
| #if defined(__ELF__) | |
| if (__tgt_register_rpc_callback) | |
| __tgt_register_rpc_callback(HandleRPCOpcodes); | |
| #elif defined(__APPLE__) || defined(_MSC_VER) | |
| __tgt_register_rpc_callback(HandleRPCOpcodes); | |
| #endif | |
| } | |
| } // namespace Fortran::runtime::io |