/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*!
 * \file session.h
 * \brief This file serves as the entry point of Disco and defines key data structures and
 * interfaces.
 *
 * Disco is a distributed runtime that consists of a controler and a cluster of workers. The
 * controler is responsible for managing the workers by broadcasting commands to all the workers
 * together, and the workers are responsible for executing the commands and. The controler and
 * workers communicate with each other through a bi-directional channel.
 *
 * Different from a generic system, Disco is designed to as "single-program-multiple-data" (SPMD)
 * runtime, which means that all the workers execute the same instruction at the same time, but the
 * data they are working on may be different. For example, in data parallelism, each worker may
 * work on a different batches of the data, but they all execute the same set of instructions.
 * Therefore, imagine there is a virtual machine that executes the program, the structures of
 * workers' register files could be considered as "identical" (single program) although the values
 * may differ (multiple data).
 *
 *
 * **DRef.** Following the design above, consider the program in SPMD in a virtual ISA, then each
 * worker is a virtual machine instance to execute the ISA maintaining its own register file.
 * The controler denotes each of their register files with a unique integer "register id",
 * and the workers use this id to refer to the register file that resides on itself.
 * DRef is a control-side object backed by such a register id. The data it contains is not assumed
 * to be directly accessible by the controler, with an exception for worker-0, which is a special
 * worker that is always co-located with the controler.
 *
 * **Worker-0.** Worker-0 is a special worker that is always co-located with the controler.
 * It is assumed that the controler can synchronize with and access the registers of worker-0.
 * The Disco session provides multiple APIs to interact specifically with the worker-0.
 * To shared data with other workers, a common paradigm in Disco is to copy data from the
 * controler-side Tensor to the worker-0, and then copy it to other workers using primitives on
 * the data plane, for example, `broadcast` and `send`.
 *
 * **Control plane.** The controler broadcasts commands to all the workers as control signals.
 * For example, the control may ask all workers to load a library or call a function respectively.
 * Common control signals include: shutdown, retrievel a global ffi::Function, call packed function,
 * etc. The controler is assumed to keep a message channel to each worker to implement the broadcast
 * behavior, and the message channel may vary depends on usecases.
 *
 * **Data plane.** The data channel is usually used to exchange data between workers, especially for
 * tensor data which is usually large. For example, performing an allreduce operator for sharded
 * matrix multiplication, or broadcasting for an input tensor. For efficiency, the data channel is
 * usually backed by NCCL on NVIDIA GPUs, RCCL on AMD GPUs, or MPI on CPUs.
 *
 * **Session.** A Disco session is a primary interface to interact with the Disco runtime, serving
 * as a global context that manages the control and workers. It could be implemented as a
 * multi-threaded with a pool of workers for single-node multi-gpu scenarios, or TCP sockets for
 * workloads that span over a cluster of nodes.
 *
 * **Channel.** Disco channel is a bi-directional communication channel between the controler and
 * workers for exchanging control signals. It is no different from a generic RPC channel, but
 * adopts TVM's ffi::Function calling convention to support polymorphic and variadic arguments.
 */
#ifndef TVM_RUNTIME_DISCO_SESSION_H_
#define TVM_RUNTIME_DISCO_SESSION_H_

#include <tvm/ffi/container/shape.h>
#include <tvm/ffi/function.h>
#include <tvm/runtime/tensor.h>

#include <mutex>
#include <queue>
#include <string>
#include <utility>

namespace tvm {
namespace runtime {

/*!
 * \brief Static FFI type index for `runtime::disco::DRef`.
 *
 * Allocated within the [kTVMFFIDynObjectBegin - 16, kTVMFFIDynObjectBegin)
 * custom-static slot range. The sibling constant `kRuntimeRPCObjectRef`
 * lives in `src/runtime/rpc/rpc_session.h` and uses `... - 13`; values must
 * remain disjoint across this small reserved block.
 */
constexpr int32_t kRuntimeDiscoDRef = TVMFFITypeIndex::kTVMFFIDynObjectBegin - 14;

static_assert(kRuntimeDiscoDRef >= TVMFFITypeIndex::kTVMFFIStaticObjectEnd &&
                  kRuntimeDiscoDRef < TVMFFITypeIndex::kTVMFFIDynObjectBegin,
              "kRuntimeDiscoDRef must live in the static custom-index slot range");

/*!
 * \brief All possible kinds of Disco commands.
 */
enum class DiscoAction : int32_t {
  kShutDown = 0,
  kKillReg = 1,
  kGetGlobalFunc = 2,
  kCallPacked = 3,
  kSyncWorker = 4,
  kCopyFromWorker0 = 5,
  kCopyToWorker0 = 6,
  kDebugGetFromRemote = 7,
  kDebugSetRegister = 8,
};

/*! \brief Converts the enum class `DiscoAction` to string */
inline std::string DiscoAction2String(DiscoAction action) {
  switch (action) {
    case DiscoAction::kShutDown:
      return "kShutDown";
    case DiscoAction::kKillReg:
      return "kKillReg";
    case DiscoAction::kGetGlobalFunc:
      return "kGetGlobalFunc";
    case DiscoAction::kCallPacked:
      return "kCallPacked";
    case DiscoAction::kSyncWorker:
      return "kSyncWorker";
    case DiscoAction::kCopyFromWorker0:
      return "kCopyFromWorker0";
    case DiscoAction::kCopyToWorker0:
      return "kCopyToWorker0";
    case DiscoAction::kDebugGetFromRemote:
      return "kDebugGetFromRemote";
    case DiscoAction::kDebugSetRegister:
      return "kDebugSetRegister";
  }
  TVM_FFI_THROW(ValueError) << "Unknown DiscoAction: " << static_cast<int>(action);
}

class SessionObj;

/*!
 * \brief An object that exists on all workers.
 *
 * The controler assigns a unique "register id" to each object, and the worker uses this id to
 * refer to the object residing on itself.
 */
class DRefObj : public ffi::Object {
 public:
  /*!\ brief Send dellocation command for `reg_id` */
  inline ~DRefObj();
  /*!
   * \brief Get the value of a DRef from a remote worker.
   * \param worker_id The id of the worker to be fetched from.
   * \return The value of the register.
   */
  inline ffi::Any DebugGetFromRemote(int worker_id);
  /*!
   * \brief Copy from the Tensor provided to a remote worker.
   * \param worker_id The id of the worker to be copied to.
   * \param source The Tensor to be copied.
   */
  inline void DebugCopyFrom(int worker_id, ffi::AnyView source);

  static constexpr const uint32_t _type_index = kRuntimeDiscoDRef;
  static const constexpr bool _type_final = true;
  static constexpr const bool _type_mutable = true;
  TVM_FFI_DECLARE_OBJECT_INFO_STATIC("runtime.disco.DRef", DRefObj, ffi::Object);

  /*! \brief The id of the register */
  int64_t reg_id;
  /*! \brief Back-pointer to the host controler session */
  ffi::ObjectRef session{nullptr};

 private:
  inline SessionObj* GetSession();
};

/*!
 * \brief Managed reference to DRefObj.
 * \sa DRefObj
 * \note No public constructor is provided as it is not supposed to be directly created by users.
 */
class DRef : public ffi::ObjectRef {
 public:
  explicit DRef(ffi::ObjectPtr<DRefObj> data) : ffi::ObjectRef(data) {
    TVM_FFI_ICHECK(data != nullptr);
  }
  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(DRef, ffi::ObjectRef, DRefObj);
};

/*!
 * \brief A Disco interactive session. It allows users to interact with the Disco command queue with
 * various ffi::Function calling convention.
 */
class SessionObj : public ffi::Object {
 public:
  virtual ~SessionObj() = default;
  /*!
   * \brief Call a ffi::Function on workers providing variadic arguments.
   * \tparam Args In the variadic arguments, the supported types include:
   * - integers and floating point numbers;
   * - DataType;
   * - Device;
   * - std::string;
   * - DRef.
   * Examples of unsupported types:
   * - Tensor, DLTensor;
   * - TVM Objects, including ffi::Function, Module and String;
   * \param func The function to be called.
   * \param args The variadic arguments.
   * \return The return value of function call
   */
  template <typename... Args>
  TVM_FFI_INLINE DRef CallPacked(const DRef& func, Args&&... args);
  /*!
   * \brief Call packed function on each worker using a packed sequence. The calling convention:
   * The first element must be DiscoAction::kCallPacked,
   * The second element must be 0, which will later be updated by the session to return reg_id
   * The thirtd element is the function to be called.
   */
  TVM_RUNTIME_DLL virtual DRef CallWithPacked(const ffi::PackedArgs& args) = 0;
  /*! \brief Get the number of workers in the session. */
  TVM_RUNTIME_DLL virtual int64_t GetNumWorkers() = 0;
  /*! \brief Get a global functions on workers. */
  TVM_RUNTIME_DLL virtual DRef GetGlobalFunc(const std::string& name) = 0;
  /*!
   * \brief Copy an Tensor from worker-0 to the controler-side Tensor
   * \param host_array The array to be copied to worker-0
   * \param remote_array The Tensor on worker-0
   */
  TVM_RUNTIME_DLL virtual void CopyFromWorker0(const Tensor& host_array,
                                               const DRef& remote_array) = 0;
  /*!
   * \brief Copy the controler-side Tensor to worker-0
   * \param host_array The array to be copied to worker-0
   * \param remote_array The Tensor on worker-0
   */
  TVM_RUNTIME_DLL virtual void CopyToWorker0(const Tensor& host_array,
                                             const DRef& remote_array) = 0;
  /*!
   * \brief Synchrnoize the controler with a worker, and it will wait until worker finishes
   * executing this instruction.
   * \param worker_id The id of the worker to be synced with.
   * \note This function is usually used for worker-0, because it is the only worker that is
   * assumed to collocate with the controler. Syncing with other workers may not be supported.
   */
  TVM_RUNTIME_DLL virtual void SyncWorker(int worker_id) = 0;
  /*! \brief Signal all the workers to shutdown */
  TVM_RUNTIME_DLL virtual void Shutdown() = 0;
  /*!
   * \brief Initialize the data plane between workers.
   * \param ccl The name of the communication backend, e.g., nccl, rccl, mpi.
   * \param device_ids The device ids of the workers.
   */
  TVM_RUNTIME_DLL virtual void InitCCL(ffi::String ccl, ffi::Shape device_ids) = 0;
  /*!
   * \brief Get the value of a register from a remote worker.
   * \param reg_id The id of the register to be fetched.
   * \param worker_id The id of the worker to be fetched from.
   * \return The value of the register.
   */
  TVM_RUNTIME_DLL virtual ffi::Any DebugGetFromRemote(int64_t reg_id, int worker_id) = 0;
  /*!
   * \brief Set the value of a register on a remote worker.
   * \param reg_id The id of the register to be set.
   * \param value The value to be set.
   * \param worker_id The id of the worker to be set.
   */
  TVM_RUNTIME_DLL virtual void DebugSetRegister(int64_t reg_id, ffi::AnyView value,
                                                int worker_id) = 0;

  struct FFI;
  friend struct SessionObj::FFI;
  friend class DRefObj;

  static constexpr const bool _type_mutable = true;
  TVM_FFI_DECLARE_OBJECT_INFO("runtime.disco.Session", SessionObj, ffi::Object);

 protected:
  /*! \brief Deallocate a register id, kill it on all workers, and append it to `free_regs_`. */
  virtual void DeallocReg(int reg_id) = 0;
};

/*!
 * \brief Managed reference to SessionObj
 * \sa SessionObj
 */
class Session : public ffi::ObjectRef {
 public:
  /*!
   * \brief Create a session backed by a thread pool of workers
   * \param num_workers The number of workers.
   * \param num_groups The number of worker groups.
   */
  TVM_RUNTIME_DLL static Session ThreadedSession(int num_workers, int num_groups);
  /*!
   * \brief Create a session backed by pipe-based multiprocessing
   * \param num_workers The number of workers.
   * \param num_groups The number of worker groups.
   * \param process_pool_creator The name of a global function that takes `num_workers` as an input,
   * and returns a ffi::Function, which takes an integer `worker_id` as the input and returns None.
   * When `worker-id` is 0, it shuts down the process pool; Otherwise, it retursn a tuple
   * (read_fd, writefd) used to communicate with the corresponding worker.
   * \param entrypoint The entrypoint of DiscoWorker main worker function.
   * \note Worker-0 is always co-located with the controler as a separate thread, and therefore
   * worker-0 does not exist in the process pool.
   */
  TVM_RUNTIME_DLL static Session ProcessSession(int num_workers, int num_groups,
                                                ffi::String process_pool_creator,
                                                ffi::String entrypoint);

  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(Session, ffi::ObjectRef, SessionObj);
};

/*!
 * \brief A bi-directional channel for controler-worker communication.
 * This channel is primarily used to transfer control messages but not data.
 */
class DiscoChannel {
 public:
  virtual ~DiscoChannel() = default;
  /*! \brief Send a packed sequence to the receiver */
  virtual void Send(const ffi::PackedArgs& args) = 0;
  /*! \brief Receive a packed sequence from worker */
  virtual ffi::PackedArgs Recv() = 0;
  /*! \brief Reply a packed sequence to the sender */
  virtual void Reply(const ffi::PackedArgs& args) = 0;
  /*! \brief Receive a reply from the worker */
  virtual ffi::PackedArgs RecvReply() = 0;
};

/*!
 * \brief A special communication channel between controler and worker-0,
 * assuming they are always collocated in the same process.
 */
class WorkerZeroData {
 public:
  /*!
   * \brief The host-side arrays to passed to worker-0 for special uses, for example,
   * copy-to-worker0 and copy-from-worker0
   */
  std::queue<Tensor> host_arrays;
  /*! \brief The mutex that guards `host_arrays` */
  std::mutex queue_mutex_;
};

// Implementation details

inline SessionObj* DRefObj::GetSession() {
  return const_cast<SessionObj*>(static_cast<const SessionObj*>(session.get()));
}

DRefObj::~DRefObj() {
  if (this->session.defined()) {
    GetSession()->DeallocReg(reg_id);
  }
}

ffi::Any DRefObj::DebugGetFromRemote(int worker_id) {
  return GetSession()->DebugGetFromRemote(this->reg_id, worker_id);
}

void DRefObj::DebugCopyFrom(int worker_id, ffi::AnyView value) {
  return GetSession()->DebugSetRegister(this->reg_id, value, worker_id);
}

template <typename... Args>
DRef SessionObj::CallPacked(const DRef& func, Args&&... args) {
  constexpr int offset = 3;
  constexpr int kNumArgs = offset + sizeof...(Args);
  ffi::AnyView packed_args[kNumArgs];
  ffi::PackedArgs::Fill(packed_args,
                        /*.0=*/static_cast<int>(DiscoAction::kCallPacked),  // action
                        /*.1=*/0,     // reg_id, which will be updated by this->CallWithPacked
                        /*.2=*/func,  // the function to be called
                        std::forward<Args>(args)...);
  return this->CallWithPacked(ffi::PackedArgs(packed_args, kNumArgs));
}

}  // namespace runtime
}  // namespace tvm
#endif  // TVM_RUNTIME_DISCO_SESSION_H_
