浏览代码

[MLA-1909] Match3 and Camera/RenderTexture sensor GC improvements (#5233)

/check-for-ModelOverriders
GitHub 3 年前
当前提交
41f38daa
共有 13 个文件被更改,包括 223 次插入86 次删除
  1. 7
      DevProject/Packages/packages-lock.json
  2. 44
      com.unity.ml-agents.extensions/Runtime/Match3/Match3Sensor.cs
  3. 28
      com.unity.ml-agents.extensions/Runtime/Match3/Match3SensorComponent.cs
  4. 12
      com.unity.ml-agents.extensions/Tests/Editor/Match3/Match3SensorTests.cs
  5. 5
      com.unity.ml-agents/CHANGELOG.md
  6. 15
      com.unity.ml-agents/Runtime/Agent.cs
  7. 58
      com.unity.ml-agents/Runtime/Sensors/CameraSensor.cs
  8. 16
      com.unity.ml-agents/Runtime/Sensors/CameraSensorComponent.cs
  9. 43
      com.unity.ml-agents/Runtime/Sensors/ObservationWriter.cs
  10. 38
      com.unity.ml-agents/Runtime/Sensors/RenderTextureSensor.cs
  11. 16
      com.unity.ml-agents/Runtime/Sensors/RenderTextureSensorComponent.cs
  12. 18
      com.unity.ml-agents/Runtime/Utilities.cs
  13. 9
      com.unity.ml-agents/Tests/Runtime/Sensor/CameraSensorComponentTest.cs

7
DevProject/Packages/packages-lock.json


"dependencies": {
"com.unity.barracuda": "1.3.2-preview",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.physics2d": "1.0.0"
"com.unity.modules.jsonserialize": "1.0.0"
}
},
"com.unity.ml-agents.extensions": {

"dependencies": {
"com.unity.ml-agents": "2.0.0-exp.1"
"com.unity.ml-agents": "2.0.0-exp.1",
"com.unity.modules.physics": "1.0.0"
}
},
"com.unity.nuget.mono-cecil": {

44
com.unity.ml-agents.extensions/Runtime/Match3/Match3Sensor.cs


using System;
using Debug = UnityEngine.Debug;
/// <summary>
/// Delegate that provides integer values at a given (x,y) coordinate.
/// </summary>

/// Sensor for Match3 games. Can generate either vector, compressed visual,
/// or uncompressed visual observations. Uses a GridValueProvider to determine the observation values.
/// </summary>
public class Match3Sensor : ISensor, IBuiltInSensor
public class Match3Sensor : ISensor, IBuiltInSensor, IDisposable
{
Match3ObservationType m_ObservationType;
ObservationSpec m_ObservationSpec;

BoardSize m_MaxBoardSize;
GridValueProvider m_GridValues;
int m_OneHotSize;
Texture2D m_ObservationTexture;
OneHotToTextureUtil m_TextureUtil;
/// <summary>
/// Create a sensor for the GridValueProvider with the specified observation type.

return offset;
}
/// <inheritdoc/>

var height = m_MaxBoardSize.Rows;
var width = m_MaxBoardSize.Columns;
var tempTexture = new Texture2D(width, height, TextureFormat.RGB24, false);
var converter = new OneHotToTextureUtil(height, width);
if (ReferenceEquals(null, m_ObservationTexture))
{
m_ObservationTexture = new Texture2D(width, height, TextureFormat.RGB24, false);
}
if (ReferenceEquals(null, m_TextureUtil))
{
m_TextureUtil = new OneHotToTextureUtil(height, width);
}
var bytesOut = new List<byte>();
var currentBoardSize = m_Board.GetCurrentBoardSize();

var numCellImages = (m_OneHotSize + 2) / 3;
for (var i = 0; i < numCellImages; i++)
{
converter.EncodeToTexture(
m_TextureUtil.EncodeToTexture(
tempTexture,
m_ObservationTexture,
bytesOut.AddRange(tempTexture.EncodeToPNG());
bytesOut.AddRange(m_ObservationTexture.EncodeToPNG());
DestroyTexture(tempTexture);
return bytesOut.ToArray();
}

return BuiltInSensorType.Match3Sensor;
}
static void DestroyTexture(Texture2D texture)
/// <summary>
/// Clean up the owned Texture2D.
/// </summary>
public void Dispose()
if (Application.isEditor)
if (!ReferenceEquals(null, m_ObservationTexture))
// Edit Mode tests complain if we use Destroy()
Object.DestroyImmediate(texture);
}
else
{
Object.Destroy(texture);
Utilities.DestroyTexture(m_ObservationTexture);
m_ObservationTexture = null;
}
}
}

int channelOffset,
int currentHeight,
int currentWidth
)
)
{
var i = 0;
// There's an implicit flip converting to PNG from texture, so make sure we

28
com.unity.ml-agents.extensions/Runtime/Match3/Match3SensorComponent.cs


using System;
using Unity.MLAgents.Sensors;
using UnityEngine;

/// Sensor component for a Match3 game.
/// </summary>
[AddComponentMenu("ML Agents/Match 3 Sensor", (int)MenuGroup.Sensors)]
public class Match3SensorComponent : SensorComponent
public class Match3SensorComponent : SensorComponent, IDisposable
{
/// <summary>
/// Name of the generated Match3Sensor object.

/// </summary>
public Match3ObservationType ObservationType = Match3ObservationType.Vector;
private ISensor[] m_Sensors;
// Clean up any existing sensors
Dispose();
return specialSensor != null ? new ISensor[] { cellSensor, specialSensor } : new ISensor[] { cellSensor };
m_Sensors = specialSensor != null
? new ISensor[] { cellSensor, specialSensor }
: new ISensor[] { cellSensor };
return m_Sensors;
/// <summary>
/// Clean up the sensors created by CreateSensors().
/// </summary>
public void Dispose()
{
if (m_Sensors != null)
{
for (var i = 0; i < m_Sensors.Length; i++)
{
((Match3Sensor)m_Sensors[i]).Dispose();
}
m_Sensors = null;
}
}
}
}

12
com.unity.ml-agents.extensions/Tests/Editor/Match3/Match3SensorTests.cs


using System.Collections.Generic;
using System.IO;
using System.Reflection;
using NUnit.Framework;
using Unity.MLAgents.Extensions.Match3;
using UnityEngine;

};
SensorTestHelper.CompareObservation(specialSensor, expectedObs3D);
}
// Test that Dispose() cleans up the component and its sensors
sensorComponent.Dispose();
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var componentSensors = (ISensor[])typeof(Match3SensorComponent).GetField("m_Sensors", flags).GetValue(sensorComponent);
Assert.IsNull(componentSensors);
var cellTexture = (Texture2D)typeof(Match3Sensor).GetField("m_ObservationTexture", flags).GetValue(cellSensor);
Assert.IsNull(cellTexture);
var specialTexture = (Texture2D)typeof(Match3Sensor).GetField("m_ObservationTexture", flags).GetValue(cellSensor);
Assert.IsNull(specialTexture);
}

5
com.unity.ml-agents/CHANGELOG.md


- `RaycastPerceptionSensor` now caches its raycast results; they can be accessed via `RayPerceptionSensor.RayPerceptionOutput`. (#5222)
- `ActionBuffers` are now reset to zero before being passed to `Agent.Heuristic()` and
`IHeuristicProvider.Heuristic()`. (#5227)
- `Agent` will now call `IDisposable.Dispose()` on all `ISensor`s that implement the `IDisposable` interface. (#5233)
- `CameraSensor`, `RenderTextureSensor`, and `Match3Sensor` will now reuse their `Texture2D`s, reducing the
amount of memory that needs to be allocated during runtime. (#5233)
- Optimzed `ObservationWriter.WriteTexture()` so that it doesn't call `Texture2D.GetPixels32()` for `RGB24` textures.
This results in much less memory being allocated during inference with `CameraSensor` and `RenderTextureSensor`. (#5233)
#### ml-agents / ml-agents-envs / gym-unity (Python)
- Some console output have been moved from `info` to `debug` and will not be printed by default. If you want all messages to be printed, you can run `mlagents-learn` with the `--debug` option or add the line `debug: true` at the top of the yaml config file. (#5211)

15
com.unity.ml-agents/Runtime/Agent.cs


Academy.Instance.AgentForceReset -= _AgentReset;
NotifyAgentDone(DoneReason.Disabled);
}
CleanupSensors();
m_Brain?.Dispose();
OnAgentDisabled?.Invoke(this);
m_Initialized = false;

"Sensor names must be unique.");
}
#endif
}
void CleanupSensors()
{
// Dispose all attached sensor
for (var i = 0; i < sensors.Count; i++)
{
var sensor = sensors[i];
if (sensor is IDisposable disposableSensor)
{
disposableSensor.Dispose();
}
}
}
void InitializeActuators()

58
com.unity.ml-agents/Runtime/Sensors/CameraSensor.cs


using System;
using UnityEngine;
using UnityEngine.Rendering;

/// A sensor that wraps a Camera object to generate visual observations for an agent.
/// </summary>
public class CameraSensor : ISensor, IBuiltInSensor
public class CameraSensor : ISensor, IBuiltInSensor, IDisposable
{
Camera m_Camera;
int m_Width;

private ObservationSpec m_ObservationSpec;
SensorCompressionType m_CompressionType;
Texture2D m_Texture;
/// <summary>
/// The Camera used for rendering the sensor observations.

set { m_CompressionType = value; }
}
/// <summary>
/// Creates and returns the camera sensor.
/// </summary>

var channels = grayscale ? 1 : 3;
m_ObservationSpec = ObservationSpec.Visual(height, width, channels, observationType);
m_CompressionType = compression;
m_Texture = new Texture2D(width, height, TextureFormat.RGB24, false);
}
/// <summary>

{
using (TimerStack.Instance.Scoped("CameraSensor.GetCompressedObservation"))
{
var texture = ObservationToTexture(m_Camera, m_Width, m_Height);
ObservationToTexture(m_Camera, m_Texture, m_Width, m_Height);
var compressed = texture.EncodeToPNG();
DestroyTexture(texture);
var compressed = m_Texture.EncodeToPNG();
return compressed;
}
}

{
using (TimerStack.Instance.Scoped("CameraSensor.WriteToTensor"))
{
var texture = ObservationToTexture(m_Camera, m_Width, m_Height);
var numWritten = writer.WriteTexture(texture, m_Grayscale);
DestroyTexture(texture);
ObservationToTexture(m_Camera, m_Texture, m_Width, m_Height);
var numWritten = writer.WriteTexture(m_Texture, m_Grayscale);
return numWritten;
}
}

/// <summary>
/// Renders a Camera instance to a 2D texture at the corresponding resolution.
/// </summary>
/// <returns>The 2D texture.</returns>
/// <param name="texture2D">Texture2D to render to.</param>
/// <returns name="texture2D">Texture2D to render to.</returns>
public static Texture2D ObservationToTexture(Camera obsCamera, int width, int height)
public static void ObservationToTexture(Camera obsCamera, Texture2D texture2D, int width, int height)
{
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null)
{

var texture2D = new Texture2D(width, height, TextureFormat.RGB24, false);
var oldRec = obsCamera.rect;
obsCamera.rect = new Rect(0f, 0f, 1f, 1f);
var depth = 24;

obsCamera.rect = oldRec;
RenderTexture.active = prevActiveRt;
RenderTexture.ReleaseTemporary(tempRt);
return texture2D;
/// <summary>
/// Computes the observation shape for a camera sensor based on the height, width
/// and grayscale flag.
/// </summary>
/// <param name="width">Width of the image captures from the camera.</param>
/// <param name="height">Height of the image captures from the camera.</param>
/// <param name="grayscale">Whether or not to convert the image to grayscale.</param>
/// <returns>The observation shape.</returns>
internal static int[] GenerateShape(int width, int height, bool grayscale)
/// <inheritdoc/>
public BuiltInSensorType GetBuiltInSensorType()
return new[] { height, width, grayscale ? 1 : 3 };
return BuiltInSensorType.CameraSensor;
static void DestroyTexture(Texture2D texture)
/// <summary>
/// Clean up the owned Texture2D.
/// </summary>
public void Dispose()
if (Application.isEditor)
if (!ReferenceEquals(null, m_Texture))
// Edit Mode tests complain if we use Destroy()
// TODO move to extension methods for UnityEngine.Object?
Object.DestroyImmediate(texture);
}
else
{
Object.Destroy(texture);
Utilities.DestroyTexture(m_Texture);
m_Texture = null;
}
/// <inheritdoc/>
public BuiltInSensorType GetBuiltInSensorType()
{
return BuiltInSensorType.CameraSensor;
}
}
}

16
com.unity.ml-agents/Runtime/Sensors/CameraSensorComponent.cs


using System;
using UnityEngine;
using UnityEngine.Serialization;

/// A SensorComponent that creates a <see cref="CameraSensor"/>.
/// </summary>
[AddComponentMenu("ML Agents/Camera Sensor", (int)MenuGroup.Sensors)]
public class CameraSensorComponent : SensorComponent
public class CameraSensorComponent : SensorComponent, IDisposable
{
[HideInInspector, SerializeField, FormerlySerializedAs("camera")]
Camera m_Camera;

/// <returns>The created <see cref="CameraSensor"/> object for this component.</returns>
public override ISensor[] CreateSensors()
{
Dispose();
m_Sensor = new CameraSensor(m_Camera, m_Width, m_Height, Grayscale, m_SensorName, m_Compression, m_ObservationType);
if (ObservationStacks != 1)

{
m_Sensor.Camera = m_Camera;
m_Sensor.CompressionType = m_Compression;
}
}
/// <summary>
/// Clean up the sensor created by CreateSensors().
/// </summary>
public void Dispose()
{
if (!ReferenceEquals(m_Sensor, null))
{
m_Sensor.Dispose();
m_Sensor = null;
}
}
}

43
com.unity.ml-agents/Runtime/Sensors/ObservationWriter.cs


Texture2D texture,
bool grayScale)
{
if (texture.format == TextureFormat.RGB24)
{
return obsWriter.WriteTextureRGB24(texture, grayScale);
}
var width = texture.width;
var height = texture.height;

for (var w = 0; w < width; w++)
{
var currentPixel = texturePixels[(height - h - 1) * width + w];
if (grayScale)
{
obsWriter[h, w, 0] =

obsWriter[h, w, 0] = currentPixel.r / 255.0f;
obsWriter[h, w, 1] = currentPixel.g / 255.0f;
obsWriter[h, w, 2] = currentPixel.b / 255.0f;
}
}
}
return height * width * (grayScale ? 1 : 3);
}
internal static int WriteTextureRGB24(
this ObservationWriter obsWriter,
Texture2D texture,
bool grayScale
)
{
var width = texture.width;
var height = texture.height;
var rawBytes = texture.GetRawTextureData<byte>();
// During training, we convert from Texture to PNG before sending to the trainer, which has the
// effect of flipping the image. We need another flip here at inference time to match this.
for (var h = height - 1; h >= 0; h--)
{
for (var w = 0; w < width; w++)
{
var offset = (height - h - 1) * width + w;
var r = rawBytes[3 * offset];
var g = rawBytes[3 * offset + 1];
var b = rawBytes[3 * offset + 2];
if (grayScale)
{
obsWriter[h, w, 0] = (r + g + b) / 3f / 255.0f;
}
else
{
// For Color32, the r, g and b values are between 0 and 255.
obsWriter[h, w, 0] = r / 255.0f;
obsWriter[h, w, 1] = g / 255.0f;
obsWriter[h, w, 2] = b / 255.0f;
}
}
}

38
com.unity.ml-agents/Runtime/Sensors/RenderTextureSensor.cs


using System;
using UnityEngine;
namespace Unity.MLAgents.Sensors

/// </summary>
public class RenderTextureSensor : ISensor, IBuiltInSensor
public class RenderTextureSensor : ISensor, IBuiltInSensor, IDisposable
{
RenderTexture m_RenderTexture;
bool m_Grayscale;

Texture2D m_Texture;
/// <summary>
/// The compression type used by the sensor.

m_Name = name;
m_ObservationSpec = ObservationSpec.Visual(height, width, grayscale ? 1 : 3);
m_CompressionType = compressionType;
m_Texture = new Texture2D(width, height, TextureFormat.RGB24, false);
}
/// <inheritdoc/>

{
using (TimerStack.Instance.Scoped("RenderTextureSensor.GetCompressedObservation"))
{
var texture = ObservationToTexture(m_RenderTexture);
ObservationToTexture(m_RenderTexture, m_Texture);
var compressed = texture.EncodeToPNG();
DestroyTexture(texture);
var compressed = m_Texture.EncodeToPNG();
return compressed;
}
}

{
using (TimerStack.Instance.Scoped("RenderTextureSensor.Write"))
{
var texture = ObservationToTexture(m_RenderTexture);
var numWritten = writer.WriteTexture(texture, m_Grayscale);
DestroyTexture(texture);
ObservationToTexture(m_RenderTexture, m_Texture);
var numWritten = writer.WriteTexture(m_Texture, m_Grayscale);
return numWritten;
}
}

/// <summary>
/// Converts a RenderTexture to a 2D texture.
/// </summary>
/// <returns>The 2D texture.</returns>
/// <returns name="texture2D">Texture2D to render to.</returns>
public static Texture2D ObservationToTexture(RenderTexture obsTexture)
/// <param name="texture2D">Texture2D to render to.</param>
public static void ObservationToTexture(RenderTexture obsTexture, Texture2D texture2D)
var texture2D = new Texture2D(width, height, TextureFormat.RGB24, false);
var prevActiveRt = RenderTexture.active;
RenderTexture.active = obsTexture;

RenderTexture.active = prevActiveRt;
return texture2D;
static void DestroyTexture(Texture2D texture)
/// <summary>
/// Clean up the owned Texture2D.
/// </summary>
public void Dispose()
if (Application.isEditor)
if (!ReferenceEquals(null, m_Texture))
// Edit Mode tests complain if we use Destroy()
// TODO move to extension methods for UnityEngine.Object?
Object.DestroyImmediate(texture);
}
else
{
Object.Destroy(texture);
Utilities.DestroyTexture(m_Texture);
m_Texture = null;
}
}
}

16
com.unity.ml-agents/Runtime/Sensors/RenderTextureSensorComponent.cs


using System;
using UnityEngine;
using UnityEngine.Serialization;

/// Component that wraps a <see cref="RenderTextureSensor"/>.
/// </summary>
[AddComponentMenu("ML Agents/Render Texture Sensor", (int)MenuGroup.Sensors)]
public class RenderTextureSensorComponent : SensorComponent
public class RenderTextureSensorComponent : SensorComponent, IDisposable
{
RenderTextureSensor m_Sensor;

/// <inheritdoc/>
public override ISensor[] CreateSensors()
{
Dispose();
m_Sensor = new RenderTextureSensor(RenderTexture, Grayscale, SensorName, m_Compression);
if (ObservationStacks != 1)
{

if (m_Sensor != null)
{
m_Sensor.CompressionType = m_Compression;
}
}
/// <summary>
/// Clean up the sensor created by CreateSensors().
/// </summary>
public void Dispose()
{
if (!ReferenceEquals(null, m_Sensor))
{
m_Sensor.Dispose();
m_Sensor = null;
}
}
}

18
com.unity.ml-agents/Runtime/Utilities.cs


using System;
using System.Diagnostics;
using UnityEngine;
namespace Unity.MLAgents
{

result[actionIndex + 1] = runningSum;
}
return result;
}
/// <summary>
/// Safely destroy a texture. This has to be used differently in unit tests.
/// </summary>
/// <param name="texture"></param>
internal static void DestroyTexture(Texture2D texture)
{
if (Application.isEditor)
{
// Edit Mode tests complain if we use Destroy()
UnityEngine.Object.DestroyImmediate(texture);
}
else
{
UnityEngine.Object.Destroy(texture);
}
}
[Conditional("DEBUG")]

9
com.unity.ml-agents/Tests/Runtime/Sensor/CameraSensorComponentTest.cs


using System;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using Unity.MLAgents.Sensors;

var expectedShape = new InplaceArray<int>(height, width, grayscale ? 1 : 3);
Assert.AreEqual(expectedShape, sensor.GetObservationSpec().Shape);
Assert.AreEqual(typeof(CameraSensor), sensor.GetType());
// Make sure cleaning up the component cleans up the sensor too
cameraComponent.Dispose();
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var cameraComponentSensor = (CameraSensor)typeof(CameraSensorComponent).GetField("m_Sensor", flags).GetValue(cameraComponent);
Assert.IsNull(cameraComponentSensor);
var cameraTexture = (Texture2D)typeof(CameraSensor).GetField("m_Texture", flags).GetValue(sensor);
Assert.IsNull(cameraTexture);
}
}
}
正在加载...
取消
保存