Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, '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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, '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 > 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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, '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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, '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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, '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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh
, '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

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment - #6305

Merged
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit
Sep 8, 2022
Merged

Add SetMaximumMemoryUsageInMegaByte in AutoMLExperiment#6305
LittleLittleCloud merged 23 commits into
dotnet:mainfrom
LittleLittleCloud:u/xiaoyun/memoryLimit

Conversation

@LittleLittleCloud

@LittleLittleCloudLittleLittleCloud commented Aug 26, 2022

Copy link
Copy Markdown
Member

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

This PR adds SetMaximumMemoryUsageInMegaByte, which sets limit to the maximum memory a trial can use, and cancel that running trial if it exceeds the maximum memory limitation.

#6293

Example

// set maximum memory usage to 8gbexperiment.SetMaximumMemoryUsageInMegaByte(8*1024);

How it work

AutoMLExperiment monitors the memory and cpu usage periodically (the default period is 2 seconds) using IPerformanceMonitor, and once the trial exceed the boundary, AutoMLExperiment cancel that trial via MLContext.CancelExecution and does clean up.

Known limitation

This requires estimator support cancel during training by checking if context is still alive from time to time, which is not true for all estimators in ML.Net.
For example, LGBM can't check if context is alive in unmanaged code part, which makes it can't be cancelled during the time when it's calling into native LightGbm dll. In that situation, tiral memory usage might still exceed maximum limit.

@LittleLittleCloudLittleLittleCloud changed the title U/xiaoyun/memory limitAdd SetMaximumMemoryUsageInMegaByte in AutoMLExperimentAug 29, 2022
@luisquintanilla

Copy link
Copy Markdown
Contributor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

@LittleLittleCloud looks good to me.

What happens when memory usage hits the specified limit? Is the entire experiment cancelled or only that currently running trial? Does enforcing the memory limit mean that GC kicks in more frequently?

Only current trial will be cancelled

GC will be called every time a trial get completed to clean up and release memory, no matter if that trial is successful or not. So enforcing memory limit won't affect GC kick frequency.

@codecov

codecovBot commented Aug 29, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6305 (5496403) into main (9652e59) will increase coverage by 0.06%.
The diff coverage is 92.90%.

Additional details and impacted files
@@ Coverage Diff @@## main #6305 +/- ##
==========================================
+ Coverage 68.56% 68.63% +0.06% 
==========================================
Files 1170 1170 Lines 247158 247310 +152 Branches 25675 25683 +8 ==========================================
+ Hits 169475 169732 +257 + Misses 70940 70849 -91 + Partials 6743 6729 -14 
FlagCoverage Δ
Debug68.63% <92.90%> (+0.06%)⬆️
production63.06% <100.00%> (+0.06%)⬆️
test89.05% <92.41%> (-0.02%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...Microsoft.ML.AutoML.Tests/AutoMLExperimentTests.cs92.38% <87.64%> (-7.62%)⬇️
src/Microsoft.ML.FastTree/FastTree.cs80.48% <100.00%> (+0.07%)⬆️
src/Microsoft.ML.LightGbm/LightGbmTrainerBase.cs79.77% <100.00%> (+0.30%)⬆️
...c/Microsoft.ML.LightGbm/WrappedLightGbmTraining.cs45.45% <100.00%> (+0.71%)⬆️
...osoft.ML.Tests/TrainerEstimators/TreeEstimators.cs97.80% <100.00%> (+0.19%)⬆️
src/Microsoft.ML.Maml/MAML.cs24.36% <0.00%> (-2.54%)⬇️
src/Microsoft.ML.Data/Utils/LossFunctions.cs67.35% <0.00%> (+0.51%)⬆️
...oft.ML.StandardTrainers/Standard/SdcaMulticlass.cs92.49% <0.00%> (+1.02%)⬆️
... and 4 more

@@ -332,7 +338,7 @@ private SweepablePipeline CreateBinaryClassificationPipeline(IDataView trainData

internal class BinaryClassificationRunner : ITrialRunner

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.

BinaryClassificationRunner

Should we use IDisposable with this class?

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.

That's a great question here. The IDisposable pattern is mainly because I want to make sure MLContext.CancelExecuation get called and set to null after the trial is finished while I don't want to explicitly call it's deconstructor or call GC. But I can go another route if you have any recommendation.

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.

Dispose pattern is not only for GC finalization or deconstruction. It is used to clean up any object when done using it. You already implemented the Dispose method. Having IDisposable will benefit with the using () code pattern.

Comment threadsrc/Microsoft.ML.AutoML/API/BinaryClassificationExperiment.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs Outdated
return this;
}

public AutoMLExperiment SetMaximumMemoryUsageInMegaByte(double value = double.MaxValue)

@tarekghtarekghAug 31, 2022

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.

value

what happen if someone passed NaN value? can you try it and look what you'll get?

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.

I will add a check here to make sure it's either not NaN or non-positive.

@tarekgh

tarekgh commented Aug 31, 2022

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

what happen if 2 threads called this in same time? or this is not supported scenario?

Reply

It's not designed to support on AutoMLExperiment level. So if user wants to launch several AutoMLExperiment on multi-thread, they need to create new AutoMLExperiment to do that.


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

_settings.CancellationToken = ct;
_cts.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);
_settings.CancellationToken.Register(() => _cts.Cancel());
_globalCancellationTokenSource.CancelAfter((int)_settings.MaxExperimentTimeInSeconds * 1000);

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.

)_settings.MaxExperimentTimeInSeconds * 1000

Any concern with overflowing this calculation?

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.

Thanks for catching that, switch to TimeSpan.FromSeconds to avoid overflow in int

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.

Resolved

@tarekgh

Copy link
Copy Markdown
Member
 public async Task<TrialResult> RunAsync(CancellationToken ct = default)

Also, Did we think making _globalCancellationTokenSource not class field and only having it as local inside RunAsync? The current code cause problems in multi-threading scenarios.


In reply to: 1233449717


Refers to: src/Microsoft.ML.AutoML/AutoMLExperiment/AutoMLExperiment.cs:234 in 16eeeae. [](commit_id = 16eeeae, deletion_comment = False)

Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
Comment threadsrc/Microsoft.ML.AutoML/AutoMLExperiment/IPerformanceMonitor.cs Outdated
var currentCpuProcessorTime = Process.GetCurrentProcess().TotalProcessorTime;
var elapseCpuProcessorTime = currentCpuProcessorTime - _totalCpuProcessorTime;
var cpuUsedMs = elapseCpuProcessorTime.TotalMilliseconds;
var cpuUsageInTotal = cpuUsedMs / (Environment.ProcessorCount * _checkIntervalInMilliseconds);

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.

Environment.ProcessorCount * _checkIntervalInMilliseconds

nit: This can be calculated once in the constructor.

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.

compiler should be able to optimize that.

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.

Are you assuming that? or you have tried it?

Trying quickly in csharplab I am seeing it is not optimized. note I didn't use Environment.ProcessorCount because it is just 1 and will be optimized at that time. so, I used other constant.

Anyway, it is a minor point, and you can ignore it if you are ok with the current code.

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.

Yeah indeed it's not, turns out I'm too optimistic on the capability of how much optimization compiler can be.

Thanks for introducing csharplab, that's such a wonderful tool

trialCancellationTokenSource.Cancel();

GC.AddMemoryPressure(Convert.ToInt64(m) * 1024 * 1024);
GC.Collect();

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.

why we are doing that?

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.

You mean GC.Collect or GC.AddMemoryPressure part?

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.

yes

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 because if we hit that code, it means the current trial uses memory that exceed the limit. Therefore we would like to run a GC to collect memory after that trial get cancelled.

GC.AddMemoryPressure is mostly for LightGbm which might allocate large chunk of unmanaged memory.

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.

Resolved

@tarekghtarekghSep 1, 2022

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.

Wouldn't GC kick off automatically when it needs more memory? at that time will collect any stale objects. I am asking why you are forcing that to happen at this time.

@LittleLittleCloudLittleLittleCloudSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory. And in that case, the next trial will likely to be canceled even if it's not using a lot of memory because the memory used by the last trial is not released.

@tarekghtarekghSep 1, 2022

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.

But it wouldn't if GC thinks it still has enough memory.

Could you please talk more about that? Why GC will think there will not be enough memory? I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

I am not objecting here, I am trying to understand the case.

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.

I expect GC will run to collect any unneeded objects at that time which should free a lot of memory. no?

Only if GC thinks there's not enough memory and runs a collection, but that's not always the case when a trial gets canceled. For example, if the user sets a very small number for maximum memory usage.

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.

I admit I am not fully understanding the whole picture :-) I'll leave it to you if you are sure about that. You may ignore my comment then.

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@LittleLittleCloud
LittleLittleCloud merged commit e99dfd4 into dotnet:mainSep 8, 2022
@ghostghost locked as resolved and limited conversation to collaborators Oct 8, 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.

3 participants

@LittleLittleCloud@luisquintanilla@tarekgh