Skip to content

Commit dfe9f3a

Browse files
adamsitniksafern
authored andcommitted
Support for custom metrics reported in the Benchmarks (#735)
* simplify and cleanup the code, remove dead code * use `Target` to specify that given setup method should be executed for selected benchmarks, not all * consume the result of Predict to make sure it does not get dead-code eliminated * reference input files from .csproj and copy them to output dir, don't rely on hardcoded folder hierarchy * every ML.NET benchmark allocates a lot of memory and should be executed in a dedicated process * make it possible for every type to report different metrics * enforce current culture as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",") * for our time consuming benchmarks 1 warmup iteration is enough * workaround for the auto-generated code to avoid name coflict for Microsoft.ML.Runtime.IHost and BenchmarkDotNet.Engines.IHost.. * add comment about why we need a custom toolchain * update BDN version to allow benchmarking with CoreRun * code review fix: spacing
1 parent 5133797 commit dfe9f3a

File tree

7 files changed

+291
-137
lines changed

7 files changed

+291
-137
lines changed

build/Dependencies.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<LightGBMPackageVersion>2.1.2.2</LightGBMPackageVersion>
1111
<MlNetMklDepsPackageVersion>0.0.0.5</MlNetMklDepsPackageVersion>
1212
<SystemDrawingCommonPackageVersion>4.5.0</SystemDrawingCommonPackageVersion>
13-
<BenchmarkDotNetVersion>0.11.0</BenchmarkDotNetVersion>
13+
<BenchmarkDotNetVersion>0.11.1</BenchmarkDotNetVersion>
1414
<TensorFlowVersion>1.10.0</TensorFlowVersion>
1515
</PropertyGroup>
1616
</Project>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using BenchmarkDotNet.Attributes;
6+
using BenchmarkDotNet.Columns;
7+
using BenchmarkDotNet.Reports;
8+
using BenchmarkDotNet.Running;
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Linq;
12+
using System.Text;
13+
14+
namespace Microsoft.ML.Benchmarks
15+
{
16+
public abstract class WithExtraMetrics
17+
{
18+
protected abstract IEnumerable<Metric> GetMetrics();
19+
20+
/// <summary>
21+
/// this method is executed after running the benchmrks
22+
/// we use it as hack to simply print to console so ExtraMetricColumn can parse the output
23+
/// </summary>
24+
[GlobalCleanup]
25+
public void ReportMetrics()
26+
{
27+
foreach (var metric in GetMetrics())
28+
{
29+
Console.WriteLine(metric.ToParsableString());
30+
}
31+
}
32+
}
33+
34+
public class ExtraMetricColumn : IColumn
35+
{
36+
public string ColumnName => "Extra Metric";
37+
public string Id => nameof(ExtraMetricColumn);
38+
public string Legend => "Value of the provided extra metric";
39+
public bool IsNumeric => true;
40+
public bool IsDefault(Summary summary, BenchmarkCase benchmark) => true;
41+
public bool IsAvailable(Summary summary) => true;
42+
public bool AlwaysShow => true;
43+
public ColumnCategory Category => ColumnCategory.Custom;
44+
public int PriorityInCategory => 1;
45+
public UnitType UnitType => UnitType.Dimensionless;
46+
public string GetValue(Summary summary, BenchmarkCase benchmark) => GetValue(summary, benchmark, null);
47+
public override string ToString() => ColumnName;
48+
49+
public string GetValue(Summary summary, BenchmarkCase benchmark, ISummaryStyle style)
50+
{
51+
if (!summary.HasReport(benchmark))
52+
return "-";
53+
54+
var results = summary[benchmark].ExecuteResults;
55+
if (results.Count != 1)
56+
return "-";
57+
58+
var result = results.Single();
59+
var buffer = new StringBuilder();
60+
61+
foreach (var line in result.ExtraOutput)
62+
{
63+
if (Metric.TryParse(line, out Metric metric))
64+
{
65+
if (buffer.Length > 0)
66+
buffer.Append(", ");
67+
68+
buffer.Append(metric.ToColumnValue());
69+
}
70+
}
71+
72+
return buffer.Length > 0 ? buffer.ToString() : "-";
73+
}
74+
}
75+
76+
public struct Metric
77+
{
78+
private const string Prefix = "// Metric";
79+
private const char Separator = '#';
80+
81+
public string Name { get; }
82+
public string Value { get; }
83+
84+
public Metric(string name, string value) : this()
85+
{
86+
Name = name;
87+
Value = value;
88+
}
89+
90+
public string ToColumnValue()
91+
=> $"{Name}: {Value}";
92+
93+
public string ToParsableString()
94+
=> $"{Prefix} {Separator} {Name} {Separator} {Value}";
95+
96+
public static bool TryParse(string line, out Metric metric)
97+
{
98+
metric = default;
99+
100+
if (!line.StartsWith(Prefix))
101+
return false;
102+
103+
var splitted = line.Split(Separator);
104+
105+
metric = new Metric(splitted[1].Trim(), splitted[2].Trim());
106+
107+
return true;
108+
}
109+
}
110+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using BenchmarkDotNet.Extensions;
6+
using BenchmarkDotNet.Toolchains;
7+
using BenchmarkDotNet.Toolchains.CsProj;
8+
using System;
9+
using System.IO;
10+
using System.Linq;
11+
12+
namespace Microsoft.ML.Benchmarks.Harness
13+
{
14+
/// <summary>
15+
/// to avoid side effects of benchmarks affect each other BenchmarkDotNet runs every benchmark in a standalone, dedicated process
16+
/// however to do that it needs to be able to create, build and run new executable
17+
///
18+
/// the problem with ML.NET is that it has native dependencies, which are NOT copied by MSBuild to the output folder
19+
/// in case where A has native dependency and B references A
20+
///
21+
/// this is why this class exists: to copy the native dependencies to folder with .exe
22+
/// </summary>
23+
public class ProjectGenerator : CsProjGenerator
24+
{
25+
public ProjectGenerator(string targetFrameworkMoniker) : base(targetFrameworkMoniker, platform => platform.ToConfig(), null)
26+
{
27+
}
28+
29+
protected override void CopyAllRequiredFiles(ArtifactsPaths artifactsPaths)
30+
{
31+
base.CopyAllRequiredFiles(artifactsPaths);
32+
33+
CopyMissingNativeDependencies(artifactsPaths);
34+
}
35+
36+
private void CopyMissingNativeDependencies(ArtifactsPaths artifactsPaths)
37+
{
38+
var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
39+
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);
40+
41+
foreach (var nativeDependency in Directory
42+
.EnumerateFiles(folderWithNativeDependencies)
43+
.Where(fileName => ContainsWithIgnoreCase(fileName, "native")))
44+
{
45+
File.Copy(
46+
sourceFileName: nativeDependency,
47+
destFileName: Path.Combine(foldeWithAutogeneratedExe, Path.GetFileName(nativeDependency)),
48+
overwrite: true);
49+
}
50+
}
51+
52+
bool ContainsWithIgnoreCase(string text, string word) => text != null && text.IndexOf(word, StringComparison.InvariantCultureIgnoreCase) >= 0;
53+
}
54+
}

test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using BenchmarkDotNet.Attributes;
66
using Microsoft.ML.Runtime;
7+
using Microsoft.ML.Runtime.Internal.Calibration;
78
using Microsoft.ML.Runtime.Data;
89
using Microsoft.ML.Runtime.EntryPoints;
910
using Microsoft.ML.Runtime.KMeans;
@@ -13,21 +14,11 @@ namespace Microsoft.ML.Benchmarks
1314
{
1415
public class KMeansAndLogisticRegressionBench
1516
{
16-
private static string s_dataPath;
17+
private readonly string _dataPath = Program.GetInvariantCultureDataPath("adult.train");
1718

1819
[Benchmark]
19-
public IPredictor TrainKMeansAndLR() => TrainKMeansAndLRCore();
20-
21-
[GlobalSetup]
22-
public void Setup()
20+
public ParameterMixingCalibratedPredictor TrainKMeansAndLR()
2321
{
24-
s_dataPath = Program.GetDataPath("adult.train");
25-
}
26-
27-
private static IPredictor TrainKMeansAndLRCore()
28-
{
29-
string dataPath = s_dataPath;
30-
3122
using (var env = new TlcEnvironment(seed: 1))
3223
{
3324
// Pipeline
@@ -53,7 +44,7 @@ private static IPredictor TrainKMeansAndLRCore()
5344
new TextLoader.Range() { Min = 10, Max = 12 }
5445
})
5546
}
56-
}, new MultiFileSource(dataPath));
47+
}, new MultiFileSource(_dataPath));
5748

5849
IDataTransform trans = CategoricalTransform.Create(env, new CategoricalTransform.Arguments
5950
{
@@ -83,4 +74,4 @@ private static IPredictor TrainKMeansAndLRCore()
8374
}
8475
}
8576
}
86-
}
77+
}

test/Microsoft.ML.Benchmarks/Microsoft.ML.Benchmarks.csproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,16 @@
2020
<ItemGroup>
2121
<NativeAssemblyReference Include="CpuMathNative" />
2222
</ItemGroup>
23+
<ItemGroup>
24+
<Folder Include="Input\" />
25+
<Content Include="..\data\iris.txt" Link="Input\iris.txt">
26+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
27+
</Content>
28+
<None Include="..\data\adult.train" Link="Input\adult.train">
29+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
30+
</None>
31+
<None Include="..\data\wikipedia-detox-250-line-data.tsv" Link="Input\wikipedia-detox-250-line-data.tsv">
32+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
33+
</None>
34+
</ItemGroup>
2335
</Project>

test/Microsoft.ML.Benchmarks/Program.cs

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
using BenchmarkDotNet.Diagnosers;
77
using BenchmarkDotNet.Jobs;
88
using BenchmarkDotNet.Running;
9-
using BenchmarkDotNet.Columns;
10-
using BenchmarkDotNet.Reports;
11-
using BenchmarkDotNet.Toolchains.InProcess;
9+
using BenchmarkDotNet.Toolchains;
10+
using BenchmarkDotNet.Toolchains.CsProj;
11+
using BenchmarkDotNet.Toolchains.DotNetCli;
12+
using Microsoft.ML.Benchmarks.Harness;
13+
using System.Globalization;
1214
using System.IO;
13-
using Microsoft.ML.Models;
15+
using System.Threading;
1416

1517
namespace Microsoft.ML.Benchmarks
1618
{
@@ -28,52 +30,33 @@ static void Main(string[] args)
2830
private static IConfig CreateCustomConfig()
2931
=> DefaultConfig.Instance
3032
.With(Job.Default
33+
.WithWarmupCount(1) // for our time consuming benchmarks 1 warmup iteration is enough
3134
.WithMaxIterationCount(20)
32-
.With(InProcessToolchain.Instance))
33-
.With(new ClassificationMetricsColumn("AccuracyMacro", "Macro-average accuracy of the model"))
35+
.With(CreateToolchain()))
36+
.With(new ExtraMetricColumn())
3437
.With(MemoryDiagnoser.Default);
3538

36-
internal static string GetDataPath(string name)
37-
=> Path.GetFullPath(Path.Combine(_dataRoot, name));
38-
39-
static readonly string _dataRoot;
40-
static Program()
39+
/// <summary>
40+
/// we need our own toolchain because MSBuild by default does not copy recursive native dependencies to the output
41+
/// </summary>
42+
private static IToolchain CreateToolchain()
4143
{
42-
var currentAssemblyLocation = new FileInfo(typeof(Program).Assembly.Location);
43-
var rootDir = currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName;
44-
_dataRoot = Path.Combine(rootDir, "test", "data");
44+
var csProj = CsProjCoreToolchain.Current.Value;
45+
var tfm = NetCoreAppSettings.Current.Value.TargetFrameworkMoniker;
46+
47+
return new Toolchain(
48+
tfm,
49+
new ProjectGenerator(tfm),
50+
csProj.Builder,
51+
csProj.Executor);
4552
}
46-
}
47-
48-
public class ClassificationMetricsColumn : IColumn
49-
{
50-
private readonly string _metricName;
51-
private readonly string _legend;
5253

53-
public ClassificationMetricsColumn(string metricName, string legend)
54+
internal static string GetInvariantCultureDataPath(string name)
5455
{
55-
_metricName = metricName;
56-
_legend = legend;
57-
}
58-
59-
public string ColumnName => _metricName;
60-
public string Id => _metricName;
61-
public string Legend => _legend;
62-
public bool IsNumeric => true;
63-
public bool IsDefault(Summary summary, BenchmarkCase benchmark) => true;
64-
public bool IsAvailable(Summary summary) => true;
65-
public bool AlwaysShow => true;
66-
public ColumnCategory Category => ColumnCategory.Custom;
67-
public int PriorityInCategory => 1;
68-
public UnitType UnitType => UnitType.Dimensionless;
56+
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
57+
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
6958

70-
public string GetValue(Summary summary, BenchmarkCase benchmark, ISummaryStyle style)
71-
{
72-
var property = typeof(ClassificationMetrics).GetProperty(_metricName);
73-
return property.GetValue(StochasticDualCoordinateAscentClassifierBench.s_metrics).ToString();
59+
return Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "Input", name);
7460
}
75-
public string GetValue(Summary summary, BenchmarkCase benchmark) => GetValue(summary, benchmark, null);
76-
77-
public override string ToString() => ColumnName;
7861
}
7962
}

0 commit comments

Comments
 (0)