| File: PredictionEnginePoolTests.cs | Web Access |
| Project: src\test\Microsoft.Extensions.ML.Tests\Microsoft.Extensions.ML.Tests.csproj (Microsoft.Extensions.ML.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.IO; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.ML; using Microsoft.ML.Data; using Microsoft.ML.TestFramework; using Microsoft.ML.TestFrameworkCommon; using Xunit; using Xunit.Abstractions; namespace Microsoft.Extensions.ML { public class PredictionEnginePoolTests : BaseTestClass { public PredictionEnginePoolTests(ITestOutputHelper output) : base(output) { } [Fact] public void can_load_namedmodel() { var services = new ServiceCollection() .AddOptions() .AddLogging(); services.AddPredictionEnginePool<SentimentData, SentimentPrediction>() .FromFile(modelName: "model1", filePath: Path.Combine("TestModels", "SentimentModel.zip"), watchForChanges: false); var sp = services.BuildServiceProvider(); var pool = sp.GetRequiredService<PredictionEnginePool<SentimentData, SentimentPrediction>>(); var model = pool.GetModel("model1"); Assert.NotNull(model); } [Fact] public void pool_serves_predictions_across_a_hot_swap() { var loader = CreateReloadableLoader(); using var pool = CreatePool(loader); var inFlight = pool.GetPredictionEngine(); Assert.NotNull(inFlight.Predict(new SentimentData { SentimentText = "great" })); loader.Reload(); var afterSwap = pool.GetPredictionEngine(); Assert.NotNull(afterSwap.Predict(new SentimentData { SentimentText = "terrible" })); pool.ReturnPredictionEngine(afterSwap); pool.ReturnPredictionEngine(inFlight); var reused = pool.GetPredictionEngine(); Assert.NotSame(inFlight, reused); Assert.NotNull(reused.Predict(new SentimentData { SentimentText = "fine" })); pool.ReturnPredictionEngine(reused); } [Fact] public void disposing_pool_releases_loader_resources() { var loader = CreateReloadableLoader(); var pool = CreatePool(loader); _ = pool.GetPredictionEngine(); pool.Dispose(); loader.Reload(); pool.Dispose(); } [Fact] public void pooled_engines_do_not_dispose_the_shared_model() { var context = new MLContext(seed: 1); using var stream = File.OpenRead(Path.Combine("TestModels", "SentimentModel.zip")); var innerModel = context.Model.Load(stream, out _); var model = new DisposeCountingTransformer(innerModel); var loader = new ReloadableModelLoader(model); using var pool = CreatePool(loader); var maximumRetained = Environment.ProcessorCount * 2; var rented = new PredictionEngine<SentimentData, SentimentPrediction>[maximumRetained + 2]; for (var i = 0; i < rented.Length; i++) { rented[i] = pool.GetPredictionEngine(); } foreach (var engine in rented) { pool.ReturnPredictionEngine(engine); } Assert.Equal(0, model.DisposeCount); var afterOverflow = pool.GetPredictionEngine(); Assert.NotNull(afterOverflow.Predict(new SentimentData { SentimentText = "still works" })); pool.ReturnPredictionEngine(afterOverflow); } private static ReloadableModelLoader CreateReloadableLoader() { var context = new MLContext(seed: 1); using var stream = File.OpenRead(Path.Combine("TestModels", "SentimentModel.zip")); var model = context.Model.Load(stream, out _); return new ReloadableModelLoader(model); } private static PredictionEnginePool<SentimentData, SentimentPrediction> CreatePool(ModelLoader loader) { var services = new ServiceCollection().AddOptions().AddLogging(); services.AddPredictionEnginePool<SentimentData, SentimentPrediction>(); services.Configure<PredictionEnginePoolOptions<SentimentData, SentimentPrediction>>( string.Empty, o => o.ModelLoader = loader); var sp = services.BuildServiceProvider(); return sp.GetRequiredService<PredictionEnginePool<SentimentData, SentimentPrediction>>(); } private sealed class ReloadableModelLoader : ModelLoader { private readonly ITransformer _model; private ModelReloadToken _token = new ModelReloadToken(); public ReloadableModelLoader(ITransformer model) => _model = model; public override IChangeToken GetReloadToken() => _token; public override ITransformer GetModel() => _model; public void Reload() { var previous = Interlocked.Exchange(ref _token, new ModelReloadToken()); previous.OnReload(); } } private sealed class DisposeCountingTransformer : ITransformer, IDisposable { private readonly ITransformer _inner; private int _disposeCount; public DisposeCountingTransformer(ITransformer inner) => _inner = inner; public int DisposeCount => _disposeCount; public bool IsRowToRowMapper => _inner.IsRowToRowMapper; public DataViewSchema GetOutputSchema(DataViewSchema inputSchema) => _inner.GetOutputSchema(inputSchema); public IDataView Transform(IDataView input) => _inner.Transform(input); public IRowToRowMapper GetRowToRowMapper(DataViewSchema inputSchema) => _inner.GetRowToRowMapper(inputSchema); public void Save(ModelSaveContext ctx) => _inner.Save(ctx); public void Dispose() { Interlocked.Increment(ref _disposeCount); (_inner as IDisposable)?.Dispose(); } } public class SentimentData { [ColumnName("Label"), LoadColumn(0)] public bool Sentiment; [LoadColumn(1)] public string SentimentText; } public class SentimentPrediction { [ColumnName("PredictedLabel")] public bool Sentiment; public float Score; } } }