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;
        }
    }
}