#define ENABLE_BARRACUDA
#if ENABLE_BARRACUDA
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Barracuda;
using UnityEngine;
using Tensor = MLAgents.InferenceBrain.Tensor;
namespace MLAgents.InferenceBrain
{
///
/// Prepares the Tensors for the Learning Brain and exposes a list of failed checks if Model
/// and BrainParameters are incompatible.
///
public class BarracudaModelParamLoader
{
private enum ModelActionType
{
Unknown,
Discrete,
Continuous
}
private const long ApiVersion = 2;
private IWorker _engine;
private Model _model;
private BrainParameters _brainParameters;
private List _failedModelChecks = new List();
///
/// Factory for the ModelParamLoader : Creates a ModelParamLoader and runs the checks
/// on it.
///
/// The Barracuda engine worker we get the parameters and the checks from
///
/// The Barracuda engine model for loading static parameters
///
/// The BrainParamters that are used verify the
/// compatibility with the InferenceEngine
///
public static BarracudaModelParamLoader GetLoaderAndCheck(IWorker engine, Model model,
BrainParameters brainParameters)
{
BarracudaModelParamLoader modelParamLoader = new BarracudaModelParamLoader(engine, model, brainParameters);
modelParamLoader.GenerateChecks();
return modelParamLoader;
}
private BarracudaModelParamLoader(IWorker engine, Model model, BrainParameters brainParameters)
{
_engine = engine;
_model = model;
_brainParameters = brainParameters;
}
///
/// Generates the Tensor inputs that are expected to be present in the Model.
///
/// Tensor IEnumerable with the expected Tensor inputs
public IReadOnlyList GetInputTensors()
{
List tensors = new List();
if (_model == null)
return tensors;
foreach (var input in _model.inputs)
{
tensors.Add(new Tensor
{
Name = input.name,
ValueType = Tensor.TensorType.FloatingPoint,
Data = null,
Shape = input.shape.Select(i => (long)i).ToArray()
});
}
foreach (var mem in _model.memories)
{
//Debug.Log($"{mem.input}: {mem.shape} -> {BarracudaUtils.FromBarracuda(mem.shape).Length}");
tensors.Add(new Tensor
{
Name = mem.input,
ValueType = Tensor.TensorType.FloatingPoint,
Data = null,
Shape = BarracudaUtils.FromBarracuda(mem.shape)
});
}
tensors.Sort((el1, el2) => el1.Name.CompareTo(el2.Name));
return tensors;
}
///
/// Generates the Tensor outputs that are expected to be present in the Model.
///
/// Tensor IEnumerable with the expected Tensor outputs
public string[] GetOutputNames()
{
var names = new List();
if (_model == null)
return names.ToArray();
names.Add(TensorNames.ActionOutput);
var memory = GetIntScalar(TensorNames.MemorySize);
if (memory > 0)
{
names.Add(TensorNames.RecurrentOutput_C);
names.Add(TensorNames.RecurrentOutput_H);
}
names.Sort();
return names.ToArray();
}
///
/// Queries the InferenceEngine for the value of a variable in the graph given its name.
/// Only works with int32 Tensors with zero dimensions containing a unique element.
/// If the node was not found or could not be retrieved, the value -1 will be returned.
///
/// The name of the Tensor variable
/// The value of the scalar variable in the model. (-1 if not found)
private int GetIntScalar(string name)
{
return (int)_model.GetTensorByName(name)[0];
}
///
/// Retrieves an IEnumerable of string corresponding to the failed compatibility checks
/// between the InferenceEngine and the BrainParameters.
///
public IEnumerable GetChecks()
{
return _failedModelChecks;
}
///
/// Generates the list of failed checks that failed when comparing the data from the Model
/// and from the BrainParameters
///
private void GenerateChecks()
{
_failedModelChecks.Clear();
if (_engine == null)
{
_failedModelChecks.Add(
"There is no model for this Brain, cannot run inference. " +
"(But can still train)");
return;
}
var modelApiVersion = GetIntScalar(TensorNames.VersionNumber);
var memorySize = GetIntScalar(TensorNames.MemorySize);
var isContinuousInt = GetIntScalar(TensorNames.IsContinuousControl);
var isContinuous = GetActionType(isContinuousInt);
var actionSize = GetIntScalar(TensorNames.ActionOutputShape);
if (modelApiVersion == -1)
{
_failedModelChecks.Add(
"Model was not trained using the right version of ML-Agents. Cannot use this " +
"model.");
return;
}
if (modelApiVersion != ApiVersion)
{
_failedModelChecks.Add(
$"Version of the trainer the model was trained with ({modelApiVersion}) " +
$"is not compatible with the Brain's version ({ApiVersion}).");
return;
}
CheckIntScalarPresenceHelper(new Dictionary()
{
{TensorNames.MemorySize, memorySize},
{TensorNames.IsContinuousControl, isContinuousInt},
{TensorNames.ActionOutputShape, actionSize}
});
CheckInputTensorPresence(memorySize, isContinuous);
CheckOutputTensorPresence(memorySize);
CheckInputTensorShape();
CheckOutputTensorShape(isContinuous, actionSize);
}
///
/// Converts the integer value in the model corresponding to the type of control to a
/// ModelActionType.
///
/// The integer value in the model indicating the
/// type of control
/// The equivalent ModelActionType
private static ModelActionType GetActionType(int isContinuousInt)
{
ModelActionType isContinuous;
switch (isContinuousInt)
{
case 0:
isContinuous = ModelActionType.Discrete;
break;
case 1:
isContinuous = ModelActionType.Continuous;
break;
default:
isContinuous = ModelActionType.Unknown;
break;
}
return isContinuous;
}
///
/// Given a Dictionary of node names to int values, create checks if the values have the
/// invalid value of -1.
///
/// Mapping from node names to int values
private void CheckIntScalarPresenceHelper(Dictionary requiredScalarFields)
{
foreach(var field in requiredScalarFields)
if (field.Value == -1)
{
_failedModelChecks.Add(
$"Missing node in the model provided : {field.Key}");
}
}
///
/// Generates failed checks that correspond to inputs expected by the model that are not
/// present in the BrainParameters.
///
/// The memory size that the model is expecting/
/// Whether the model is expecting continuous or
/// discrete control.
/// A IEnumerable of string corresponding to the failed input presence
/// checks.
private void CheckInputTensorPresence(int memory, ModelActionType isContinuous)
{
var tensorsNames = GetInputTensors().Select(x => x.Name).ToList();
// If there is no Vector Observation Input but the Brain Parameters expect one.
if ((_brainParameters.vectorObservationSize != 0) &&
(!tensorsNames.Contains(TensorNames.VectorObservationPlacholder)))
{
_failedModelChecks.Add(
"The model does not contain a Vector Observation Placeholder Input. " +
"You must set the Vector Observation Space Size to 0.");
}
// If there are not enough Visual Observation Input compared to what the
// Brain Parameters expect.
for (var visObsIndex = 0;
visObsIndex < _brainParameters.cameraResolutions.Length;
visObsIndex++)
{
if (!tensorsNames.Contains(
TensorNames.VisualObservationPlaceholderPrefix + visObsIndex))
{
_failedModelChecks.Add(
"The model does not contain a Visual Observation Placeholder Input " +
"for visual observation "+visObsIndex+".");
}
}
// If the model has a non-negative memory size but requires a recurrent input
if (memory > 0)
{
if (!tensorsNames.Contains(TensorNames.RecurrentInPlaceholder_H) ||
!tensorsNames.Contains(TensorNames.RecurrentInPlaceholder_C))
{
_failedModelChecks.Add(
"The model does not contain a Recurrent Input Node but has memory_size.");
}
}
// If the model uses discrete control but does not have an input for action masks
if (isContinuous == ModelActionType.Discrete)
{
if (!tensorsNames.Contains(TensorNames.ActionMaskPlaceholder))
{
_failedModelChecks.Add(
"The model does not contain an Action Mask but is using Discrete Control.");
}
}
}
///
/// Generates failed checks that correspond to outputs expected by the model that are not
/// present in the BrainParameters.
///
/// The memory size that the model is expecting/
/// A IEnumerable of string corresponding to the failed output presence
/// checks.
private void CheckOutputTensorPresence(int memory)
{
// If there is no Action Output.
if (!_model.outputs.Contains(TensorNames.ActionOutput))
{
_failedModelChecks.Add("The model does not contain an Action Output Node.");
}
// If there is no Recurrent Output but the model is Recurrent.
if (memory > 0)
{
var memOutputs = _model.memories.Select(x => x.output).ToList();
if (!memOutputs.Contains(TensorNames.RecurrentOutput_H) ||
!memOutputs.Contains(TensorNames.RecurrentOutput_C))
{
_failedModelChecks.Add(
"The model does not contain a Recurrent Output Node but has memory_size.");
}
}
}
///
/// Generates failed checks that correspond to inputs shapes incompatibilities between
/// the model and the BrainParameters.
///
private void CheckInputTensorShape()
{
var tensorTester =
new Dictionary>()
{
{TensorNames.VectorObservationPlacholder, CheckVectorObsShape},
{TensorNames.PreviousActionPlaceholder, CheckPreviousActionShape},
{TensorNames.RandomNormalEpsilonPlaceholder, ((tensor) => null)},
{TensorNames.ActionMaskPlaceholder, ((tensor) => null)},
{TensorNames.SequenceLengthPlaceholder, ((tensor) => null)},
{TensorNames.RecurrentInPlaceholder_H, ((tensor) => null)},
{TensorNames.RecurrentInPlaceholder_C, ((tensor) => null)},
};
for (var obsIndex = 0; obsIndex < _brainParameters.cameraResolutions.Length; obsIndex++)
{
var index = obsIndex;
tensorTester[TensorNames.VisualObservationPlaceholderPrefix + obsIndex] =
(tensor) => CheckVisualObsShape(tensor, index);
}
// If the model expects an input but it is not in this list
foreach (var tensor in GetInputTensors())
{
if (!tensorTester.ContainsKey(tensor.Name))
{
_failedModelChecks.Add(
"Model requires an unknown input named : " + tensor.Name);
}
else
{
var tester = tensorTester[tensor.Name];
var error = tester.Invoke(tensor);
if (error != null)
{
_failedModelChecks.Add(error);
}
}
}
}
///
/// Checks that the shape of the Vector Observation input placeholder is the same in the
/// model and in the Brain Parameters.
///
/// The tensor that is expected by the model
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
private string CheckVectorObsShape(Tensor tensor)
{
var vecObsSizeBp = _brainParameters.vectorObservationSize;
var numStackedVector = _brainParameters.numStackedVectorObservations;
var totalVecObsSizeT = tensor.Shape[tensor.Shape.Length - 1];
if (vecObsSizeBp * numStackedVector != totalVecObsSizeT)
{
return string.Format(
"Vector Observation Size of the model does not match. " +
"Received {0} x {1} but was expecting {2}.",
vecObsSizeBp, numStackedVector, totalVecObsSizeT);
}
return null;
}
///
/// Checks that the shape of the Previous Vector Action input placeholder is the same in the
/// model and in the Brain Parameters.
///
/// The tensor that is expected by the model
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
private string CheckPreviousActionShape(Tensor tensor)
{
var numberActionsBp = _brainParameters.vectorActionSize.Length;
var numberActionsT = tensor.Shape[tensor.Shape.Length - 1];
if (numberActionsBp != numberActionsT)
{
return string.Format(
"Previous Action Size of the model does not match. " +
"Received {0} but was expecting {1}.",
numberActionsBp, numberActionsT);
}
return null;
}
///
/// Checks that the shape of the visual observation input placeholder is the same in the
/// model and in the Brain Parameters.
///
/// The tensor that is expected by the model
/// The index of the visual observation.
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
private string CheckVisualObsShape(Tensor tensor, int visObsIndex)
{
var resolutionBp = _brainParameters.cameraResolutions[visObsIndex];
var widthBp = resolutionBp.width;
var heightBp = resolutionBp.height;
var pixelBp = resolutionBp.blackAndWhite ? 1 : 3;
var heightT = tensor.Shape[1];
var widthT = tensor.Shape[2];
var pixelT = tensor.Shape[3];
if ((widthBp != widthT) || (heightBp != heightT) || (pixelBp != pixelT))
{
return string.Format(
"The visual Observation {0} of the model does not match. " +
"Received Tensor of shape [?x{1}x{2}x{3}] but was expecting [?x{4}x{5}x{6}].",
visObsIndex, widthBp, heightBp, pixelBp, widthT, heightT, pixelT);
}
return null;
}
///
/// Generates failed checks that correspond to output shapes incompatibilities between
/// the model and the BrainParameters.
///
/// Whether the model is expecting continuous or
/// discrete control.
/// The size of the action output that is expected
/// by the model.
/// A IEnumerable of string corresponding to the incompatible shapes between
/// model and BrainParameters.
private void CheckOutputTensorShape(ModelActionType isContinuous, int modelActionSize)
{
if (isContinuous == ModelActionType.Unknown)
{
_failedModelChecks.Add(
"Cannot infer type of Control from the provided model.");
return;
}
if (isContinuous == ModelActionType.Continuous &&
_brainParameters.vectorActionSpaceType != SpaceType.continuous)
{
_failedModelChecks.Add(
"Model has been trained using Continuous Control but the Brain Parameters " +
"suggest Discrete Control.");
return;
}
if (isContinuous == ModelActionType.Discrete &&
_brainParameters.vectorActionSpaceType != SpaceType.discrete)
{
_failedModelChecks.Add(
"Model has been trained using Discrete Control but the Brain Parameters " +
"suggest Continuous Control.");
return;
}
var tensorTester = new Dictionary>();
if (_brainParameters.vectorActionSpaceType == SpaceType.continuous)
{
tensorTester[TensorNames.ActionOutput] = CheckContinuousActionOutputShape;
}
else
{
tensorTester[TensorNames.ActionOutput] = CheckDiscreteActionOutputShape;
}
// If the model expects an output but it is not in this list
foreach (var name in _model.outputs)
{
if (tensorTester.ContainsKey(name))
{
var tester = tensorTester[name];
var error = tester.Invoke(_model.GetShapeByName(name), modelActionSize);
if (error != null)
{
_failedModelChecks.Add(error);
}
}
}
}
///
/// Checks that the shape of the discrete action output is the same in the
/// model and in the Brain Parameters.
///
/// The tensor shape that is expected by the model
/// The size of the action output that is expected
/// by the model.
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
private string CheckDiscreteActionOutputShape(TensorShape shape, int modelActionSize)
{
var bpActionSize = _brainParameters.vectorActionSize.Sum();
if (modelActionSize != bpActionSize)
{
return string.Format(
"Action Size of the model does not match. " +
"The BrainParameters expect {0} but the model contains {1}.",
bpActionSize, modelActionSize);
}
return null;
}
///
/// Checks that the shape of the continuous action output is the same in the
/// model and in the Brain Parameters.
///
/// The tensor shape that is expected by the model
/// The size of the action output that is expected
/// by the model.
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
private string CheckContinuousActionOutputShape(TensorShape shape, int modelActionSize)
{
var bpActionSize = _brainParameters.vectorActionSize[0];
if (modelActionSize != bpActionSize)
{
return string.Format(
"Action Size of the model does not match. " +
"The BrainParameters expect {0} but the model contains {1}.",
bpActionSize, modelActionSize);
}
return null;
}
}
}
public class BarracudaUtils
{
private static Array LinearizeArray(Array src)
{
var elementType = src.GetType().GetElementType();
var elementSize = Marshal.SizeOf(elementType);
var dest = Array.CreateInstance(elementType, src.Length);
Buffer.BlockCopy(src, 0, dest, 0, src.Length * elementSize);
return dest;
}
protected static Barracuda.TensorShape ToBarracuda(long[] src)
{
if (src.Length > 4)
throw new NotImplementedException("Barracuda does not support Tensor shapes with rank higher than 4");
var shape = new int[4];
if (src.Length == 2)
{
shape[0] = (int)src[0];
shape[1] = 1;
shape[2] = 1;
shape[3] = (int)src[1];
}
else
{
for (var axis = 0; axis < src.Length; ++axis)
shape[shape.Length-axis-1] = (int)src[src.Length-axis-1];
}
return new Barracuda.TensorShape(shape);
}
private static float[] IntArrayToFloatArray(int[] src)
{
var dest = new float[src.Length];
for (var i = 0; i < src.Length; i++)
dest[i] = (float) src[i];
return dest;
}
public static Barracuda.Tensor ToBarracuda(MLAgents.InferenceBrain.Tensor src)
{
Array linearArray = LinearizeArray(src.Data);
if (linearArray.GetType().GetElementType() == typeof(int))
linearArray = IntArrayToFloatArray(linearArray as int[]);
var shape = ToBarracuda(src.Shape);
return new Barracuda.Tensor(shape, linearArray as float[], src.Name);
}
internal static long[] FromBarracuda(Barracuda.TensorShape src)
{
if (src.height == 1 && src.width == 1)
return new long[2] {src.batch, src.channels};
return new long[4] {src.batch, src.height, src.width, src.channels};
}
private static Array ReshapeArray(Array src, long[] shape)
{
var elementType = src.GetType().GetElementType();
var elementSize = Marshal.SizeOf(elementType);
var dest = Array.CreateInstance(elementType, shape);
Buffer.BlockCopy(src, 0, dest, 0, src.Length * elementSize);
return dest;
}
public static Tensor FromBarracuda(Barracuda.Tensor src, string nameOverride = null)
{
var shape = FromBarracuda(src.shape);
return new Tensor
{
Name = nameOverride ?? src.name,
ValueType = Tensor.TensorType.FloatingPoint,
Shape = shape,
Data = ReshapeArray(src.data.Download(src.length), shape)
};
}
}
#endif