Raw File
EvalWrapper.cpp
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
//
// EvalWrapper.cpp -- Managed code wrapping the native EvaluateModel interface
//

#include <windows.h>
#include <vcclr.h>
#include <string>
#include <utility>
#include <vector>
#include <memory>
#include <msclr\marshal_cppstd.h>

#include "CNTKException.h"
#include "EvalCommon.h"
#include "Eval.h"

#using <System.dll>
#using <System.Collections.dll>
#using <System.Drawing.dll>

using namespace std;
using namespace System;
using namespace System::Collections::Generic;
using namespace System::Collections;
using namespace System::Drawing;
using namespace System::Drawing::Imaging;
using namespace Microsoft::MSR::CNTK;

namespace Microsoft { namespace MSR { namespace CNTK { namespace Extensibility { namespace Managed {

// Used for retrieving the model appropriate for the element type (float / double)
template<typename ElemType>
using GetEvalProc = void(*)(IEvaluateModel<ElemType>**);

/// Managed wrapper for the native evaluation model
template<typename ElemType>
public ref class IEvaluateModelManaged : IDisposable
{
    typedef std::pair<std::wstring, std::vector<ElemType>*> MapEntry;

public:
    /// <summary>Initializes a new instance of the <see cref="IEvaluateModelManaged"> class.</summary>
    /// <param name="funcName">Factory function name for retrieving the native model from the dll.</param>
    IEvaluateModelManaged(String^ funcName)
    {
        try
        {
            pin_ptr <IEvaluateModel<ElemType>*> p_eval = &m_eval;
            GetEval<ElemType>(p_eval);
        }
        catch (const exception& ex)
        {
            throw gcnew CNTKException(gcnew System::String(ex.what()));
        }
    }

    /// <summary>Initializes the model evaluation library with a CNTK configuration</summary>
    /// <param name="config">Model configuration entries</param>
    void Init(String^ config)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        msclr::interop::marshal_context context;
        const std::string stdConfig = context.marshal_as<std::string>(config);

        try
        {
            m_eval->Init(stdConfig);
        }
        catch (const exception& ex)
        {
            throw GetCustomException(ex);
        }
    }

    /// <summary>Creates a network based on the network description in the configuration</summary>
    /// <param name="networkDescription">The configuration file containing the network description</param>
    void CreateNetwork(String^ networkDescription)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        msclr::interop::marshal_context context;
        const std::string stdNetworkDescription = context.marshal_as<std::string>(networkDescription);

        try
        {
            m_eval->CreateNetwork(stdNetworkDescription);
        }
        catch (const exception& ex)
        {
            throw GetCustomException(ex);
        }
    }

    /// <summary>Creates a network based on the network description in the configuration</summary>
    /// <param name="networkDescription">The configuration file containing the network description</param>
    /// <param name="outputNodeNames">The output list of nodes (replaces the model's list of output nodes)</param>
    void CreateNetwork(String^ networkDescription, List<String^>^ outputNodeNames)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        String^ outputNodeNamesProperty = outputNodeNames != nullptr ? String::Concat("outputNodeNames=", String::Join(":", outputNodeNames)) : "";
        String^ newNetworkConfig = String::Format("{0}\n{1}", outputNodeNamesProperty, networkDescription);
        this->CreateNetwork(newNetworkConfig);
    }

    /// <summary>Creates a network based on the network description in the configuration</summary>
    /// <param name="networkDescription">The configuration file containing the network description</param>
    /// <param name="deviceId">The device ID to specify for the network</param>
    void CreateNetwork(String^ networkDescription, int deviceId)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        this->CreateNetwork(networkDescription, deviceId, nullptr);
    }

    /// <summary>Creates a network based on the network description in the configuration</summary>
    /// <param name="networkDescription">The configuration file containing the network description</param>
    /// <param name="deviceId">The device ID to specify for the network</param>
    /// <param name="outputNodeNames">The output list of nodes (replaces the model's list of output nodes)</param>
    void CreateNetwork(String^ networkDescription, int deviceId, List<String^>^ outputNodeNames)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        String^ outputNodeNamesProperty = outputNodeNames != nullptr ? String::Concat("outputNodeNames=", String::Join(":", outputNodeNames)) : "";
        String^ newNetworkConfig = String::Format("deviceId={0}\n{1}\n{2}", deviceId, outputNodeNamesProperty, networkDescription);
        this->CreateNetwork(newNetworkConfig);
    }

    /// <summary>Evaluates the model using a single forward feed pass and retrieves the output layer data</summary>
    /// <param name="outputKey">The output layer name</param>
    /// <param name="outputSize">The dimension size of the output layer</param>
    /// <returns>Results for specified layer</returns>
    __declspec(deprecated) List<ElemType>^ Evaluate(String^ outputKey, int outputSize)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        try
        {
            List<ElemType>^ outputs = gcnew List<ElemType>(outputSize);
            for (int i = 0; i < outputSize; i++)
            {
                outputs->Add(*(gcnew ElemType));
            }

            Dictionary<String^, List<ElemType>^>^ outputMap = gcnew Dictionary<String^, List<ElemType>^>();
            outputMap->Add(outputKey, outputs);

            Evaluate(outputMap);
            return outputMap[outputKey];
        }
        catch (Exception^)
        {
            throw;
        }
    }

    /// <summary>Evaluates the model using a single forward feed pass and retrieves the output layer data</summary>
    /// <param name="outputKey">The output layer name</param>
    /// <returns>Results for specified layer</returns>
    List<ElemType>^ Evaluate(String^ outputKey)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        try
        {
            int outputSize = GetNodeDimensions(NodeGroup::Output)[outputKey];

            List<ElemType>^ outputs = gcnew List<ElemType>(outputSize);
            for (int i = 0; i < outputSize; i++)
            {
                outputs->Add(*(gcnew ElemType));
            }

            Dictionary<String^, List<ElemType>^>^ outputMap = gcnew Dictionary<String^, List<ElemType>^>();
            outputMap->Add(outputKey, outputs);

            Evaluate(outputMap);
            return outputMap[outputKey];
        }
        catch (Exception^)
        {
            throw;
        }
    }

    /// <summary>Evaluates the model against input data and retrieves the output layer data</summary>
    /// <param name="inputs">The input nodes and their values</param>
    /// <param name="outputs">The output nodes and their values</param>
    void Evaluate(Dictionary<String^, List<ElemType>^>^ inputs, Dictionary<String^, List<ElemType>^>^ outputs)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        std::map<std::wstring, std::vector<ElemType>*> stdInputs;
        std::map<std::wstring, std::vector<ElemType>*> stdOutputs;

        try
        {
            std::vector<shared_ptr<std::vector<ElemType>>> sharedInputVectors;
            std::vector<shared_ptr<std::vector<ElemType>>> sharedOutputVectors;

            for each (auto item in inputs)
            {
                pin_ptr<const WCHAR> key = PtrToStringChars(item.Key);
                shared_ptr<std::vector<ElemType>> ptr = CopyList(item.Value);
                sharedInputVectors.push_back(ptr);
                stdInputs.insert(MapEntry(key, ptr.get()));
            }

            for each (auto item in outputs)
            {
                pin_ptr<const WCHAR> key = PtrToStringChars(item.Key);
                shared_ptr<std::vector<ElemType>> ptr = CopyList(item.Value);
                sharedOutputVectors.push_back(ptr);
                stdOutputs.insert(MapEntry(key, ptr.get()));
            }

            try
            {
                m_eval->Evaluate(stdInputs, stdOutputs);
            }
            catch (const exception& ex)
            {
                throw GetCustomException(ex);
            }

            CopyOutput(outputs, stdOutputs);
        }
        catch (Exception^)
        {
            throw;
        }
    }

    /// <summary>Evaluates the model against input data and retrieves the output layer data</summary>
    /// <param name="inputs">The input nodes and their values</param>
    /// <param name="outputKey">The output layer name</param>
    /// <param name="outputSize">The dimension size of the output layer</param>
    /// <returns>Results for specified layer</returns>
    __declspec(deprecated) List<ElemType>^ Evaluate(Dictionary<String^, List<ElemType>^>^ inputs, String^ outputKey, int outputSize)
    {
        List<ElemType>^ outputs = gcnew List<ElemType>(outputSize);
        for (int i = 0; i < outputSize; i++)
        {
            outputs->Add(*(gcnew ElemType));
        }

        Dictionary<String^, List<ElemType>^>^ outputMap = gcnew Dictionary<String^, List<ElemType>^>();
        outputMap->Add(outputKey, outputs);

        Evaluate(inputs, outputMap);
        return outputMap[outputKey];
    }

    /// <summary>Evaluates the model against the given bitmap input, and retrieves the output layer data.
    /// The image is expected to be in RGB format, and must already be re-sized to match the network size.
    /// The feature vector that is generated will contain 3 channels.</summary>
    /// <param name="image">The image to work with.</param>
    /// <param name="outputKey">The name of the output node to retrieve.</param>
    /// <returns>Results for specified layer</returns>
    List<ElemType>^ EvaluateRgbImage(Bitmap^ image, String^ outputKey)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }
        bool hasAlphaChannel;
        if (image->PixelFormat == PixelFormat::Format24bppRgb)
        {
            hasAlphaChannel = false;
        }
        else if (image->PixelFormat == PixelFormat::Format32bppArgb)
        {
            hasAlphaChannel = true;
        }
        else
        {
            throw gcnew ArgumentException("Pixel format of input bitmap is not recognized, must be one of { Format24bppRgb, Format32bppArgb}.", "image");
        }
        int imageWidth = image->Width;
        int imageHeight = image->Height;
        // The total number of pixels in one channel of the image.
        int channelStride = imageWidth * imageHeight;
        // The number of color channels that will be fed into the network.
        int numChannels = 3;
        // The total number of pixels in all channels of the image.
        int numPixels = channelStride * numChannels;
        // A dictionary that contains the dimensions of each output node.
        auto outDims = GetNodeDimensions(NodeGroup::Output);
        // The dimensions of the requested output node.
        int outputSize;
        if (!outDims->TryGetValue(outputKey, outputSize))
        {
            auto message = String::Format("The specified output key '{0}' is not an output node of the network", outputKey);
            throw gcnew ArgumentException(message, "outputKey");
        }
        // A dictionary that contains the names of input nodes, and their dimensionality.
        auto inDims = GetNodeDimensions(NodeGroup::Input);
        if (inDims->Count != 1)
        {
            throw gcnew InvalidOperationException("The loaded network must contain exactly 1 input node.");
        }
        // Read out the single element in the dictionary. The key is the input node name,
        // value is the dimensionality.
        auto enumerator = inDims->GetEnumerator();
        enumerator.MoveNext();
        String^ inputNodeName = enumerator.Current.Key;
        int inputSize = enumerator.Current.Value;
        // #pixels * #channels in the image must match the input dimension of the network.
        if (inputSize != numPixels)
        {
            auto message = String::Format("Input image has invalid size. Expected an image with Width * Height = {0}, but got Width = {1}, Height = {2}",
                inputSize / numChannels, imageWidth, imageHeight);
            throw gcnew ArgumentException(message, "image");
        }
        // Get the native bitmap structure that is underlying the Bitmap object:
        // Need to lock the whole image into memory.
        auto rect = gcnew System::Drawing::Rectangle(0, 0, imageWidth, imageHeight);
        auto bitmap = image->LockBits(*rect, ImageLockMode::ReadOnly, image->PixelFormat);
        // The byte array that contains the bitmap.
        auto bytes = reinterpret_cast<byte*>(bitmap->Scan0.ToPointer());
        // The offset to go from one scanline of the image to the next one.
        int bitmapStride = bitmap->Stride;
        // The feature vector that will be fed into the network.
        auto featureVector = new std::vector<ElemType>(numPixels);
        int index;
        // Copy from the Bitmap byte array to the arrangement that CNTK expects:
        // First comes the R plane, then G, then B.
        for (int c = 0; c < 3; c++)
        {
            for (int h = 0; h < imageHeight; h++)
            {
                for (int w = 0; w < imageWidth; w++)
                {
                    // In the input image, each pixel is represented
                    // by R, G, B, [A] bytes
                    if (hasAlphaChannel)
                    {
                        index = h * bitmapStride + w * 4 + c;
                    }
                    else
                    {
                        index = h * bitmapStride + w * 3 + c;
                    }
                    (*featureVector)[channelStride * c + imageWidth * h + w] = (ElemType)(bytes[index]);
                }
            }
        }
        image->UnlockBits(bitmap);

        std::map<std::wstring, std::vector<ElemType>*> stdInputs;
        std::map<std::wstring, std::vector<ElemType>*> stdOutputs;
        // The CLI structure that will be returned to the caller.
        auto outputList = gcnew List<ElemType>(outputSize);
        std::vector<shared_ptr<std::vector<ElemType>>> sharedOutputVectors;
        pin_ptr<const WCHAR> inputKey = PtrToStringChars(inputNodeName);
        shared_ptr<std::vector<ElemType>> f2(featureVector);
        stdInputs.insert(MapEntry(inputKey, f2.get()));

        pin_ptr<const WCHAR> key = PtrToStringChars(outputKey);
        // Do we have to initialize the output nodes?
        shared_ptr<std::vector<ElemType>> ptr(new std::vector<ElemType>(outputSize));
        sharedOutputVectors.push_back(ptr);
        stdOutputs.insert(MapEntry(key, ptr.get()));
        try
        {
            m_eval->Evaluate(stdInputs, stdOutputs);
        }
        catch (const exception& ex)
        {
            throw GetCustomException(ex);
        }

        auto &refVec = *stdOutputs[key];
        for (auto& vec : refVec)
        {
            // List has been pre-allocated to the right size,
            // so this should be fast.
            outputList->Add(vec);
        }
        return outputList;
    }

    /// <summary>Evaluates the model against input data and retrieves the desired output layer data</summary>
    /// <param name="inputs">The input nodes and their values</param>
    /// <param name="outputKey">The output layer name</param>
    /// <returns>Results for requested layer</returns>
    List<ElemType>^ Evaluate(Dictionary<String^, List<ElemType>^>^ inputs, String^ outputKey)
    {
        auto outDims = GetNodeDimensions(NodeGroup::Output);
        int outputSize = outDims[outputKey];

        List<ElemType>^ outputs = gcnew List<ElemType>(outputSize);
        for (int i = 0; i < outputSize; i++)
        {
            outputs->Add(*(gcnew ElemType));
        }

        Dictionary<String^, List<ElemType>^>^ outputMap = gcnew Dictionary<String^, List<ElemType>^>();
        outputMap->Add(outputKey, outputs);

        Evaluate(inputs, outputMap);
        return outputMap[outputKey];
    }

    /// <summary>Returns the layer(s) and associated dimensions for the specified node group
    /// <param name="nodeGroup">The node type to query for</param>
    /// <returns>A dictionary mapping layer names to their dimension</returns>
    Dictionary<String^, int>^ GetNodeDimensions(NodeGroup nodeGroup)
    {
        if (m_eval == nullptr)
        {
            throw gcnew ObjectDisposedException("Object has been disposed.");
        }

        std::map<std::wstring, size_t> stdDims;

        try
        {
            Microsoft::MSR::CNTK::NodeGroup gr(GetNodeGroup(nodeGroup));
            m_eval->GetNodeDimensions(stdDims, gr);
        }
        catch (const exception& ex)
        {
            throw GetCustomException(ex);
        }

        Dictionary<String^, int>^ dims = gcnew Dictionary<String^, int>();

        for (auto& map_item : stdDims)
        {
            String^ key = gcnew String(map_item.first.c_str());
            int dim = static_cast<int>(map_item.second);
            dims->Add(key, dim);
        }

        return dims;
    }

    ~IEvaluateModelManaged()
    {
        if (m_eval == nullptr)
        {
            return;
        }

        this->!IEvaluateModelManaged();
    }

protected:
    !IEvaluateModelManaged()
    {
        if (m_eval != nullptr)
        {
            m_eval->Destroy();
            m_eval = nullptr;
        }
    }

private:
    // Native model evaluation instance
    IEvaluateModel<ElemType> *m_eval;

    /// <summary>Copies a list of element types from a CLI structure to a native structure</summary>
    /// <param name="list">The CLI list of items</param>
    /// <returns>A native vector of items</returns>
    shared_ptr<std::vector<ElemType>> CopyList(List<ElemType>^ list)
    {
        shared_ptr<std::vector<ElemType>> lower(new std::vector<ElemType>());
        if (list != nullptr)
        {
            for each (ElemType item in list)
            {
                lower->push_back(item);
            }
        }
        return lower;
    }

    /// <summary>Evaluates the model using a single forward feed pass without input and retrieves the output layer data</summary>
    /// <param name="outputs">The output nodes and output buffers</param>
    /// <returns>none</returns>
    void Evaluate(Dictionary<String^, List<ElemType>^>^ outputs)
    {
        std::vector<shared_ptr<std::vector<ElemType>>> sharedOutputVectors;
        std::map<std::wstring, std::vector<ElemType>*> stdOutputs;

        for each (auto item in outputs)
        {
            pin_ptr<const WCHAR> key = PtrToStringChars(item.Key);
            shared_ptr<std::vector<ElemType>> ptr = CopyList(item.Value);
            sharedOutputVectors.push_back(ptr);
            stdOutputs.insert(MapEntry(key, ptr.get()));
        }

        try
        {
            m_eval->Evaluate(stdOutputs);
        }
        catch (const exception& ex)
        {
            throw GetCustomException(ex);
        }

        CopyOutput(outputs, stdOutputs);
    }

    /// <summary>Copy output data to the output buffer</summary>
    /// <param name="outputs">The output nodes and output buffers</param>
    /// <param name="outputData">The output data</param>
    /// <returns>none</returns>
    void CopyOutput(Dictionary<String^, List<ElemType>^>^ outputs, std::map<std::wstring, std::vector<ElemType>*>& outputData)
    {
        for each (auto item in outputs)
        {
            pin_ptr<const WCHAR> key = PtrToStringChars(item.Key);
            std::vector<ElemType> *pVec = outputData[key];
            if (pVec == nullptr)
            {
                throw gcnew NullReferenceException("No output value available.");
            }

            int index = 0;
            // Copy output to CLI structure
            for (auto& vec : *pVec)
            {
                outputs[item.Key][index++] = vec;
            }
        }
    }

    /// <summary> Throws a CLR exception based on a native exception</summary>
    /// <param name="ex">The native exception to throw as a CLR exception</param>
    /// <returns>A CLR exception</returns>
    CNTKException^ GetCustomException(const exception& ex)
    {
        // Determine the appropriate exception and initialize it with the exception payload
        if (typeid(ex) == typeid(ExceptionWithCallStack<runtime_error>))
        {
            ExceptionWithCallStack<runtime_error>& rich = dynamic_cast<ExceptionWithCallStack<runtime_error>&>((runtime_error&)ex);
            return gcnew CNTKRuntimeException(gcnew System::String(rich.what()), gcnew System::String(rich.CallStack()));
        }
        else if (typeid(ex) == typeid(ExceptionWithCallStack<logic_error>))
        {
            ExceptionWithCallStack<logic_error>& rich = dynamic_cast<ExceptionWithCallStack<logic_error>&>((logic_error&)ex);
            return gcnew CNTKLogicErrorException(gcnew System::String(ex.what()), gcnew System::String(rich.CallStack()));
        }
        else if (typeid(ex) == typeid(ExceptionWithCallStack<invalid_argument>))
        {
            ExceptionWithCallStack<invalid_argument>& rich = dynamic_cast<ExceptionWithCallStack<invalid_argument>&>((invalid_argument&)ex);
            return gcnew CNTKInvalidArgumentException(gcnew System::String(ex.what()), gcnew System::String(rich.CallStack()));
        }
        else if (typeid(ex) == typeid(bad_alloc))
        {
            return gcnew CNTKBadAllocException(gcnew System::String(ex.what()));
        }
        else
        {
            return gcnew CNTKException(gcnew System::String(ex.what()));
        }
    }

    /// <summary Converts a managed (CLI) enum NodeGroup to a native NodeGroup
    /// <param name="nodeGroup">The managed (CLI) NodeGroup to convert to native</param>
    Microsoft::MSR::CNTK::NodeGroup GetNodeGroup(NodeGroup nodeGroup)
    {
        switch ((int)nodeGroup)
        {
        case Microsoft::MSR::CNTK::NodeGroup::nodeInput:
            return Microsoft::MSR::CNTK::NodeGroup::nodeInput;
        case Microsoft::MSR::CNTK::NodeGroup::nodeOutput:
            return Microsoft::MSR::CNTK::NodeGroup::nodeOutput;
        case Microsoft::MSR::CNTK::NodeGroup::nodeSpecified:
            return Microsoft::MSR::CNTK::NodeGroup::nodeSpecified;
        default:
            throw gcnew CNTKRuntimeException(String::Format("Cannot convert native NodeGroup with value: {0} to corresponding managed NodeGroup.",(int)nodeGroup), "");
        }
    }
};

/// <summary>Managed float-specific model evaluation class</summary>
/// <remarks>This class is necessary due to how generics and templates work in CLR</remarks>
public ref class IEvaluateModelManagedF : IEvaluateModelManaged<float>
{
public:
    IEvaluateModelManagedF::IEvaluateModelManagedF()
        : IEvaluateModelManaged("GetEvalF")
    {
    }
};

/// <summary>Managed double-specific model evaluation class</summary>
/// <remarks>This class is necessary due to how generics and templates work in CLR</remarks>
public ref class IEvaluateModelManagedD : IEvaluateModelManaged<double>
{
public:
    IEvaluateModelManagedD::IEvaluateModelManagedD()
        : IEvaluateModelManaged("GetEvalD")
    {
    }
};

// This method tricks the compiler into emitting the methods of the classes
// Refer to https://msdn.microsoft.com/en-us/library/ms177213.aspx for an
// explanation to this behavior
void emit()
{
    Dictionary<String^, List<float>^>^ nullDictF = nullptr;
    Dictionary<String^, List<double>^>^ nullDictD = nullptr;

    IEvaluateModelManagedF f;
    f.Init("");
    f.Evaluate(nullptr, nullDictF);
    f.Evaluate(nullptr, "");
    f.Evaluate("");
    f.EvaluateRgbImage(nullptr, "");
    f.CreateNetwork("");
    f.CreateNetwork("", 0);
    f.CreateNetwork("", nullptr);
    f.CreateNetwork("", 0, nullptr);
    f.GetNodeDimensions(NodeGroup::Specified);

    IEvaluateModelManagedD d;
    d.Init("");
    d.Evaluate(nullptr, nullDictD);
    d.Evaluate(nullptr, "");
    d.Evaluate("");
    d.EvaluateRgbImage(nullptr, "");
    d.CreateNetwork("");
    d.CreateNetwork("", 0);
    d.CreateNetwork("", nullptr);
    d.CreateNetwork("", 0,nullptr);
    d.GetNodeDimensions(NodeGroup::Specified);

    // Deprecated code, hush warnings locally only
#pragma warning(push)
#pragma warning(disable: 4996)
    f.Evaluate(nullptr, "", 0);
    f.Evaluate("", 0);
    d.Evaluate(nullptr, "", 0);
    d.Evaluate("", 0);
#pragma warning(pop)
}

}}}}}
back to top