Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Support for custom metrics reported in the Benchmarks - #735

Merged
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing
Aug 30, 2018
Merged

Support for custom metrics reported in the Benchmarks#735
safern merged 13 commits into
dotnet:masterfrom
adamsitnik:benchmarksPolishing

Conversation

@adamsitnik

Copy link
Copy Markdown
Member

This PR enables two things:

  1. executing every benchmark in an isolated process
  2. reporting custom metrics per benchmark

Why should we run every benchmark in a separate process?

  1. Because most of ML.NET benchmarks allocate a lot of memory which affect GC Generation sizes and affects final results (GC is self-tuning if we run all the benchmarks in the same process GC won't be able to find a solution that is great for all of the benchmarks)
  2. Most of the ML.NET can have potential side effects. Example: running train benchmark after running predict benchmark in the same process can possibly affect the results. With new process per benchmark, we always start at the same place and have repeatable results.

Results when running all the benchmarks in the same process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR2,134.265 ms164.3370 ms189.2507 ms16000.00009000.00003000.000049949.23 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment2,130.503 ms24.8173 ms23.2141 ms122000.000035000.00005000.0000759772.8 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris834.229 ms254.5284 ms293.1152 ms6000.00001000.0000-12173.28 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris2.472 ms0.1202 ms0.1384 ms35.156315.62503.9063123.24 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf12.712 ms0.3276 ms0.3773 ms35.156315.62503.9063123.2 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf22.370 ms0.1334 ms0.1482 ms35.156315.62503.9063123.31 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf52.492 ms0.1678 ms0.1865 ms35.156315.62503.9063123.61 KB

When running every benchmark in a dedicated process:

TypeMethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
KMeansAndLogisticRegressionBenchTrainKMeansAndLR1,968.326 ms84.3827 ms97.1753 ms16000.00009000.00003000.000050027.36 KB
StochasticDualCoordinateAscentClassifierBenchTrainIris604.496 ms238.4849 ms274.6396 ms59000.00001000.0000-76697.5 KB
StochasticDualCoordinateAscentClassifierBenchTrainSentiment1,829.670 ms10.9792 ms10.2699 ms123000.000035000.00006000.0000759758.03 KB
StochasticDualCoordinateAscentClassifierBenchPredictIris1.895 ms0.0132 ms0.0111 ms35.156315.62503.9063121.87 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf11.941 ms0.0145 ms0.0121 ms35.156315.62503.9063119.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf21.960 ms0.0676 ms0.0751 ms35.156315.62503.9063121.94 KB
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf51.870 ms0.0043 ms0.0036 ms37.109417.57813.9063120.35 KB

To run every benchmark in a standalone, dedicated process BenchmarkDotNet needs to be able to create, build and run new executable.

So far it was not possible out of the box due to MSBuild limitation. When Microsoft.ML.Benchmarks references native assembly and the auto-generated BenchmarkDotNet project references Microsoft.ML.Benchmarks the native dependencies are not copied to the output folder of the auto-generated project with benchmarks. This is why I had to implement ProjectGenerator which does that for us.

@eerhardt we had a conversation about making it possible for BenchmarkDotNet to compile ML.NET stuff a long time ago and the blocker was the native dependency.

The other thing are custom metrics. BenchmarkDotNet does not support it out of the box, I had to implement it. How it works:

  1. If given type wants to report custom metrics it has to derive from WithExtraMetrics and implement IEnumerable<Metric> GetMetrics() method
  2. WithExtraMetrics after running the benchmarks prints the custom metrics to console in child process
  3. ExtraMetricColumn parses the output in parent process.

Sample results:

TypeMethodExtra Metric
KMeansAndLogisticRegressionBenchTrainKMeansAndLR-
StochasticDualCoordinateAscentClassifierBenchTrainIris-
StochasticDualCoordinateAscentClassifierBenchTrainSentiment-
StochasticDualCoordinateAscentClassifierBenchPredictIrisAccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf1AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf2AccuracyMacro: 0.98
StochasticDualCoordinateAscentClassifierBenchPredictIrisBatchOf5AccuracyMacro: 0.98

Other changes: so far the benchmarks were using currentAssemblyLocation.Directory.Parent.Parent.Parent.Parent.FullName to get the path to folder with input files. I believe it's better to reference them as links in csproj and "copy to output directory if newer". This solution is cleaner and more futureproof.

/cc @eerhardt @danmosemsft @briancylui@KrzysztofCwalina

@shauheen

Copy link
Copy Markdown
Contributor

Thanks @adamsitnik , can you please associate this with the relevant issue?

public int PriorityInCategory => 1;
public UnitType UnitType => UnitType.Dimensionless;
// enforce Neutral Language as "en-us" because the input data files use dot as decimal separator (and it fails for cultures with ",")
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line of code is a bit surprising in a method that is supposed to return a data path. Maybe it would be better to do this in the Main method, or a GlobalSetup method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I agree that I am breaking CQRS here. My only excuse is that I have named the method GetInvariantCultureDataPath so people can expect that.

I was thinking about moving it to a [GlobalSetup] method but I am afraid that people will don't follow this pattern in new benchmarks. By having it here I guarantee that whoever is going to use files will be using CultureInfo.InvariantCulture for reading these files.

I also wonder how ML.NET samples deal with the culture info problem. Does anybody know?

@eerhardteerhardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

[GlobalCleanup]
public void ReportMetrics()
{
foreach (var metric in GetMetrics())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it improve perf to set var metrics = GetMetrics(); right before the foreach loop and then write the condition as var metric in metrics? Not sure...

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@briancylui no, it would not.

Whenever you are not sure about something you can benchmark it with BenchmarkDotNet ;)

var foldeWithAutogeneratedExe = Path.GetDirectoryName(artifactsPaths.ExecutablePath);
var folderWithNativeDependencies = Path.GetDirectoryName(typeof(ProjectGenerator).Assembly.Location);

foreach(var nativeDependency in Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing space between foreach and the succeeding (

for (int bi = 0; bi < batch.Length; bi++)
{
batch[bi] = _example;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this for loop change elements of _batches[i] or only elements of the local variable batch? Not an expert so not sure whether batch is a ref-type.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done on purpose, it's a Setup method

@briancyluibriancylui left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Feel free to merge after responding to PR comments - I don't have write access so can't hit merge unfortunately. Not an expert in Benchmark.NET, but this PR looks good to me! Thanks @adamsitnik

@briancylui

Copy link
Copy Markdown

More reviewers are needed for this PR to be merged - my review doesn't count towards mergeability since I don't have write access.

@adamsitnik

Copy link
Copy Markdown
MemberAuthor

@briancylui@eerhardt thank you for your reviews! I don't have write access myself, so who could merge it?

@shauheen there is no issue, but there was an email thread. Do you want me to create an issue for that?

@eerhardt

Copy link
Copy Markdown
Member

test OSX10.13 Debug

@briancylui

Copy link
Copy Markdown

test OSX10.13 Debug please
test public-CI please

# Conflicts:
#	build/Dependencies.props
#	test/Microsoft.ML.Benchmarks/KMeansAndLogisticRegressionBench.cs
@safern
safern merged commit dfe9f3a into dotnet:masterAug 30, 2018
@ghostghost locked as resolved and limited conversation to collaborators Mar 29, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@adamsitnik@shauheen@briancylui@eerhardt@safern