/*
 * SPDX-FileCopyrightText: Copyright (c) 1993-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed 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 SampleCuDLA.cpp
//! \brief This file contains the implementation of the cuDLA sample.
//!

// Define TRT entrypoints used in common code
#define DEFINE_TRT_ENTRYPOINTS 1
#define DEFINE_TRT_ONNX_PARSER_ENTRYPOINT 0

#include "NvInfer.h"
#include "argsParser.h"
#include "buffers.h"
#include "common.h"
#include "half.h"
#include "logger.h"

#include "cudla.h"

#include <cuda_runtime_api.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <iostream>
#include <random>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

using namespace samplesCommon;

#define CHECK_CUDLA(expr)                                                             \
    do {                                                                              \
        auto const status = (expr);                                                   \
        if (status != cudlaSuccess)                                                   \
        {                                                                             \
            sample::gLogError << "Error in " << expr << " = " << status << std::endl; \
            exit(EXIT_FAILURE);                                                                  \
        }                                                                             \
    } while (0)

std::string const gSampleName = "TensorRT.sample_cudla";

bool isDLAHeader(void const* ptr)
{
    CHECK_RETURN(ptr, false);
    char const* p = static_cast<char const*>(ptr);
    return p[4] == 'N' && p[5] == 'V' && p[6] == 'D' && p[7] == 'A';
}

#if ENABLE_DLA

class DlaContext
{
public:
    DlaContext(void* data, size_t const size)
    {
        // Initialize CUDA.
        CHECK(cudaFree(0));
        CHECK(cudaSetDevice(0));
        CHECK_CUDLA(cudlaCreateDevice(0, &mDevHandle, CUDLA_CUDA_DLA));

        // Get available devices.
        uint64_t numEngines{0};
        CHECK_CUDLA(cudlaDeviceGetCount(&numEngines));
        ASSERT(numEngines >= 1);

        // Create CUDA stream.
        CHECK(cudaStreamCreateWithFlags(&mStream, cudaStreamNonBlocking));

        // Load the module with cuDLA from the loadable data.
        deserialize(data, size);

        // Create and allocate I/O tensors with the GPU momory registered with cuDLA.
        createIoTensors();
    }

    ~DlaContext()
    {
        // Unregister the memory with cuDLA and clear vectors.
        for (auto& elem : mTensorIn)
        {
            CHECK_CUDLA(cudlaMemUnregister(mDevHandle, elem));
        }
        mTensorIn.clear();

        for (auto& elem : mTensorOut)
        {
            CHECK_CUDLA(cudlaMemUnregister(mDevHandle, elem));
        }
        mTensorOut.clear();

        // Free the buffers on GPU and clear vectors.
        for (auto& elem : mBufferGPUIn)
        {
            CHECK(cudaFree(elem));
        }
        mBufferGPUIn.clear();

        for (auto& elem : mBufferGPUOut)
        {
            CHECK(cudaFree(elem));
        }
        mBufferGPUOut.clear();

        mInputTensorDesc.clear();
        mOutputTensorDesc.clear();
        CHECK(cudaStreamDestroy(mStream));

        // Unload the module with cuDLA and destroy the device.
        CHECK_CUDLA(cudlaModuleUnload(mModuleHandle, 0));
        sample::gLogInfo << "Successfully unloaded module" << std::endl;
        CHECK_CUDLA(cudlaDestroyDevice(mDevHandle));
        sample::gLogInfo << "Device destroyed successfully" << std::endl;
    }

    //!
    //! \brief Enqueue and execute the current task.
    //!
    using BufRef = std::reference_wrapper<std::vector<half_float::half>>;
    using BufRefConst = std::reference_wrapper<std::vector<half_float::half> const>;
    void submit(std::vector<BufRefConst> const& inputBufVec, std::vector<BufRef> const& outputBufVec)
    {
        // Copy data from CPU buffers to GPU buffers.
        uint32_t const inputBufVecSize = inputBufVec.size();
        for (uint32_t i = 0; i < inputBufVecSize; ++i)
        {
            auto const& inputBuf = inputBufVec.at(i).get();
            void* inputBufferGPU = mBufferGPUIn.at(i);
            CHECK(cudaMemcpyAsync(inputBufferGPU, inputBuf.data(), mInputTensorDesc.at(i).size,
                                        cudaMemcpyHostToDevice, mStream));
        }
        // Consist of the input and output tensors in the form of the addresses registered with the DLA.
        // Ensure the registered pointers are visible to the DLA before the execution.
        mTask.moduleHandle = mModuleHandle;
        mTask.outputTensor = mTensorOut.data();
        mTask.numOutputTensors = getNbOutputTensors();
        mTask.numInputTensors = getNbInputTensors();
        mTask.inputTensor = mTensorIn.data();
        mTask.waitEvents = NULL;
        mTask.signalEvents = NULL;
        CHECK_CUDLA(cudlaSubmitTask(mDevHandle, &mTask, 1, mStream, 0));
        sample::gLogInfo << "Submitted task to DLA successfully" << std::endl;
        // Bring output buffer to CPU.
        uint32_t const outputBufVecSize = outputBufVec.size();
        for (uint32_t i = 0; i < outputBufVecSize; ++i)
        {
            auto& outputBuf = outputBufVec.at(i).get();
            void* outputBufferGPU = mBufferGPUOut.at(i);
            CHECK(cudaMemcpyAsync(outputBuf.data(), outputBufferGPU, mOutputTensorDesc.at(i).size,
                                cudaMemcpyDeviceToHost, mStream));
        }
    }

    //!
    //! \brief Wait for stream operations to finish.
    //!
    void synchronize()
    {
        CHECK(cudaStreamSynchronize(mStream));
    }

private:
    cudlaDevHandle mDevHandle;                                      //!< Device handler
    cudlaModule mModuleHandle;                                      //!< Module handler
    cudlaTask mTask;                                                //!< CuDLA task
    cudaStream_t mStream;                                           //!< CUDA stream
    std::vector<cudlaModuleTensorDescriptor> mInputTensorDesc;      //!< Input tensor descriptors
    std::vector<cudlaModuleTensorDescriptor> mOutputTensorDesc;     //!< Output tensor descriptors
    std::vector<uint64_t*> mTensorIn;                               //!< Input registered pointers
    std::vector<uint64_t*> mTensorOut;                              //!< Output registered pointers
    std::vector<void*> mBufferGPUIn;                                //!< Input allocated buffers
    std::vector<void*> mBufferGPUOut;                               //!< Output allocated buffers

    //!
    //! \brief Load the module with cuDLA from the loadable data.
    //!
    void deserialize(void* data, size_t const size)
    {
        ASSERT(isDLAHeader(static_cast<char*>(data)));
        CHECK_CUDLA(cudlaModuleLoadFromMemory(mDevHandle, static_cast<unsigned char*>(data), size, &mModuleHandle, 0));
        sample::gLogInfo << "Successfully loaded module" << std::endl;
    }

    //!
    //! \brief Get the number of input tensors.
    //!
    uint32_t getNbInputTensors() const
    {
        cudlaModuleAttribute attribute;
        CHECK_CUDLA(cudlaModuleGetAttributes(mModuleHandle, CUDLA_NUM_INPUT_TENSORS, &attribute));
        return attribute.numInputTensors;
    }

    //!
    //! \brief Get the number of output tensors.
    //!
    uint32_t getNbOutputTensors() const
    {
        cudlaModuleAttribute attribute;
        CHECK_CUDLA(cudlaModuleGetAttributes(mModuleHandle, CUDLA_NUM_OUTPUT_TENSORS, &attribute));
        return attribute.numOutputTensors;
    }

    //!
    //! \brief Allocate memory for a buffer on GPU and register the required pointer with cuDLA.
    //!
    void createMemDLA(std::vector<uint64_t*>& mTensor, std::vector<void*>& mBufferGPU, uint64_t const size, int32_t const idx)
    {
        void* bufferGPU = nullptr;
        uint64_t* bufferRegisteredPtr = nullptr;
        // Allocate memory on GPU.
        CHECK(cudaMalloc(&bufferGPU, size));
        // Register the CUDA-allocated buffers.
        CHECK_CUDLA(cudlaMemRegister(mDevHandle, static_cast<uint64_t*>(bufferGPU), size, &bufferRegisteredPtr, 0));
        CHECK(cudaMemsetAsync(bufferGPU, 0, size, mStream));
        mTensor.emplace_back(bufferRegisteredPtr);
        mBufferGPU.emplace_back(bufferGPU);
    }

    //!
    //! \brief Create and allocate I/O tensors with the GPU momory registered with cuDLA.
    //!
    void createIoTensors()
    {
        // Prepare I/O tensors
        uint32_t const numInputTensors = getNbInputTensors();
        uint32_t const numOutputTensors = getNbOutputTensors();

        // Allocate memory for input and output tensor descriptors.
        mInputTensorDesc.resize(numInputTensors);
        mOutputTensorDesc.resize(numOutputTensors);

        // Get module attributes from the loaded module.
        // Fill in the input and output tensor descriptors.
        cudlaModuleAttribute attribute;
        attribute.inputTensorDesc = mInputTensorDesc.data();
        CHECK_CUDLA(cudlaModuleGetAttributes(mModuleHandle, CUDLA_INPUT_TENSOR_DESCRIPTORS, &attribute));

        attribute.outputTensorDesc = mOutputTensorDesc.data();
        CHECK_CUDLA(cudlaModuleGetAttributes(mModuleHandle, CUDLA_OUTPUT_TENSOR_DESCRIPTORS, &attribute));

        // Get each tensor size, allocate GPU memory and register that memory with cuDLA.
        for (uint32_t i = 0; i < numInputTensors; ++i)
        {
            uint64_t const inputDescSize = mInputTensorDesc.at(i).size;
            createMemDLA(mTensorIn, mBufferGPUIn, inputDescSize, i);
        }

        for (uint32_t i = 0; i < numOutputTensors; ++i)
        {
            uint64_t const outputDescSize = mOutputTensorDesc.at(i).size;
            createMemDLA(mTensorOut, mBufferGPUOut, outputDescSize, i);
        }
    }
};

//!
//! \brief Create the single layer Network and marks the output layers.
//!
void constructNetwork(nvinfer1::INetworkDefinition& network)
{
    nvinfer1::Dims const inputDims{4, {1, 32, 32, 32}};

    auto inA = network.addInput("inputA", nvinfer1::DataType::kHALF, inputDims);
    auto inB = network.addInput("inputB", nvinfer1::DataType::kHALF, inputDims);

    auto layer = network.addElementWise(*inA, *inB, nvinfer1::ElementWiseOperation::kSUM);
    nvinfer1::ITensor* out = layer->getOutput(0);

    out->setName("output");
    network.markOutput(*out);
}

//!
//! \brief Explicitly set network I/O formats.
//!
void setNetworkIOFormats(nvinfer1::INetworkDefinition& network)
{
    nvinfer1::TensorFormat const formats = nvinfer1::TensorFormat::kCHW16;
    uint32_t const numInputs = network.getNbInputs();
    for (uint32_t i = 0; i < numInputs; i++)
    {
        auto input = network.getInput(i);
        input->setAllowedFormats(static_cast<nvinfer1::TensorFormats>(1U << static_cast<int32_t>(formats)));
    }

    uint32_t const numOutputs = network.getNbOutputs();
    for (uint32_t i = 0; i < numOutputs; i++)
    {
        auto output = network.getOutput(i);
        output->setAllowedFormats(static_cast<nvinfer1::TensorFormats>(1U << static_cast<int32_t>(formats)));
    }
}

//!
//! \brief Randomly initializes buffer.
//!
template <typename T>
void randomInit(std::vector<T>& buffer)
{
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_int_distribution<int32_t> dist(0, 63);

    auto gen = [&dist, &mt]() { return T(dist(mt)); };
    std::generate(buffer.begin(), buffer.end(), gen);
}

//!
//! \brief Verifies that the output is correct.
//!
template <typename T>
bool verifyOutput(std::vector<T> const& ref, std::vector<T> const& output)
{
    return std::equal(ref.begin(), ref.end(), output.begin());
}

//!
//! \brief Creates the network, configures the builder, and creates the network engine.
//!
//! \details This function creates a network and builds an engine to run in DLA safe mode.
//! The network consists of only one elementwise sum layer with FP16 precision.
//!
//! \return true if the engine was created successfully and false otherwise.
//!
bool build(std::unique_ptr<nvinfer1::IHostMemory>& mLoadable, nvinfer1::Dims& mInputDims, nvinfer1::Dims& mOutputDims,
    std::string const& timingCacheFile)
{
    auto builder = std::unique_ptr<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger()));
    CHECK_RETURN(builder.get(), false);

    auto network = std::unique_ptr<nvinfer1::INetworkDefinition>(builder->createNetworkV2(0));
    CHECK_RETURN(network.get(), false);

    auto config = std::unique_ptr<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
    CHECK_RETURN(config.get(), false);

    constructNetwork(*network);
    setNetworkIOFormats(*network);

    samplesCommon::enableDLA(builder.get(), config.get(), 0);

    config->clearFlag(nvinfer1::BuilderFlag::kGPU_FALLBACK);
    config->setEngineCapability(nvinfer1::EngineCapability::kDLA_STANDALONE);

    std::unique_ptr<nvinfer1::ITimingCache> timingCache{};

    // Load timing cache
    if (!timingCacheFile.empty())
    {
        timingCache = samplesCommon::buildTimingCacheFromFile(sample::gLogger.getTRTLogger(), *config, timingCacheFile);
    }

    mLoadable = std::unique_ptr<nvinfer1::IHostMemory>(builder->buildSerializedNetwork(*network, *config));
    if (!mLoadable)
    {
        return false;
    }

    if (timingCache != nullptr && !timingCacheFile.empty())
    {
        samplesCommon::updateTimingCacheFile(
            sample::gLogger.getTRTLogger(), timingCacheFile, timingCache.get(), *builder);
    }

    mInputDims = network->getInput(0)->getDimensions();
    mOutputDims = network->getOutput(0)->getDimensions();

    return true;
}
#endif // ENABLE_DLA

//!
//! \brief Prints the help information for running this sample.
//!
void printHelpInfo()
{
    sample::gLogInfo << "Usage: ./sample_cudla [-h or --help] [--timingCacheFile=<path to timing cache file>]\n";
    sample::gLogInfo << "--help             Display help information\n";
    sample::gLogInfo
        << "--timingCacheFile  Specify path to a timing cache file. If it does not already exist, it will be "
        << "created." << std::endl;
}

int main(int argc, char** argv)
{
    samplesCommon::Args args;
    bool argsOK = samplesCommon::parseArgs(args, argc, argv);
    if (!argsOK)
    {
        sample::gLogError << "Invalid arguments " << std::endl;
        printHelpInfo();
        return EXIT_FAILURE;
    }
    if (args.help)
    {
        printHelpInfo();
        return EXIT_SUCCESS;
    }

    auto sampleTest = sample::gLogger.defineTest(gSampleName, argc, argv);

    sample::gLogger.reportTestStart(sampleTest);

#if ENABLE_DLA
    std::unique_ptr<nvinfer1::IHostMemory> mLoadable{nullptr}; //!< The DLA loadable.
    nvinfer1::Dims mInputDims;                                 //!< The dimensions of the input to the network.
    nvinfer1::Dims mOutputDims;                                //!< The dimensions of the output to the network.
    CHECK_RETURN(
        build(mLoadable, mInputDims, mOutputDims, args.timingCacheFile), sample::gLogger.reportFail(sampleTest));

    int64_t const inputBufSize = samplesCommon::volume(mInputDims, 0, mInputDims.nbDims);
    int64_t const expectedOutputSize = samplesCommon::volume(mOutputDims, 0, mOutputDims.nbDims);

    // Allocate and initialize input and output buffers
    // The allocation and initialization only needs to be done once
    std::vector<half_float::half> inputBufA(inputBufSize);
    std::vector<half_float::half> inputBufB(inputBufSize);
    std::vector<half_float::half> referenceBuf(expectedOutputSize);
    std::vector<half_float::half> outputBuf(expectedOutputSize);

    randomInit(inputBufA);
    randomInit(inputBufB);

    // Create a DlaContext
    DlaContext context(mLoadable->data(), mLoadable->size());

    // The cuDLA task can be submitted more than once
    context.submit({inputBufA, inputBufB}, {outputBuf});

    context.synchronize();

    // Compute the reference output for comparision
    std::transform(
        inputBufA.begin(), inputBufA.end(), inputBufB.begin(), referenceBuf.begin(), std::plus<half_float::half>());
    CHECK_RETURN(verifyOutput(referenceBuf, outputBuf), sample::gLogger.reportFail(sampleTest));
#else  // ENABLE_DLA
    sample::gLogError << "DLA is not enabled, please compile with ENABLE_DLA=1" << std::endl;
#endif // ENABLE_DLA

    return sample::gLogger.reportPass(sampleTest);
}
