ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina
, '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

ComponentCatalog refactor - #970

Merged
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor
Sep 25, 2018
Merged

ComponentCatalog refactor#970
eerhardt merged 15 commits into
dotnet:masterfrom
eerhardt:ComponentCatalogRefactor

Conversation

@eerhardt

Copy link
Copy Markdown
Member

This executes on the plan outlined in #208 (comment) copied here for easy reading:

New Proposal

  1. We will move ComponentCatalog from being a static class to being an instance member on Environment. This has been a planned refactoring for ML.NET for a while, but hasn't been funded until now.
  2. We will completely remove any implicit scanning for components in ComponentCatalog itself. It will have public APIs to register components, but will not do any registering itself - neither by loading assemblies from disk, nor by scanning loaded assemblies.
  3. Other subsystems (like the GUI, command-line, Entry Points, and model loading) will be responsible for registering the components they require in the manner they require.
  4. During model saving, we will write the Assembly.FullName into the model file. We will then register that assembly with the env.ComponentCatalog when loading the model.
    • Any model that was saved with a previous version of ML.NET, and loaded using the API, will need to explicitly register the components before loading the model. (Or they will need to save the model again with a current version of ML.NET that will save the assembly names.)

Under normal circumstances, API users won't have to explicitly register components with the ComponentCatalog. Using the API to train models won't require looking up components from a catalog - you just create .NET objects like normal. Loading a trained model from disk will register the components inside of it by loading the Assembly and scanning it for LoadableClass assembly attributes.

Fix#208

Write the AssemblyName into the model, and use it to register the assembly during model load.
Ensure all loaded assemblies are registered in Experiment to maintain compability.
Fix tests to not use ComponentCatalog but direct instantiation instead.
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch 5 times, most recently from cd5e241 to cde333fCompareSeptember 21, 2018 15:54
@eerhardt
eerhardtforce-pushed the ComponentCatalogRefactor branch from cde333f to 430c4a7CompareSeptember 21, 2018 16:23
@justinormont

Copy link
Copy Markdown
Contributor

I logged a possibly related issue: #975"ComponentCatalog logging errors"

{
// Couldn't load as an assembly and not a zip, so warn the user.
ex = ex ?? e;
Console.Error.WriteLine("Warning: Could not load '{0}': {1}", path, ex.Message);

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.

We should not be writing to the Console from libraries. And more generally, why do we swallow this error?

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 code was copied out of the ComponentCatalog and moved to an internal class that is only called from the command line. (see src/Microsoft.ML.Maml/HelpCommand.cs and src/Microsoft.ML.ResultProcessor/ResultProcessor.cs) I wasn't going to change the behavior, because I'm sure someone is depending on it.

}
catch (Exception e)
{
ex = e;

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 do we try to load zip files? Why not either load or unzip depending on the extension?

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.

Same as above - existing behavior from the command line code that was removed from ComponentCatalog.

throw Contracts.ExceptIO(e, "Extracting extra assembly zip failed: '{0}'", path);
}

LoadAssembliesInDir(env, dir, false);

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 do we use zip files and just blast them into a folder instead using Nuget? It only works if the extensions don't use other dependencies. The moment they use other dependencies, we need to worry about versions. Nuget (theoretically) handles all of this.

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 existing behavior that I am refactoring out of ComponentCatalog so it isn't part of the public API anymore. It is only called from 2 places in the command-line (HelpCommand and ResultProcessor).

ComponentCatalog.CacheClassesExtra(_extraAssemblies);

// extra DLLs for dynamic loading
[Argument(ArgumentType.Multiple,HelpText="Extra DLLs",ShortName="dll")]
publicstring[]ExtraAssemblies=null;

}
}

public static IDisposable CreateAssemblyRegistrar(IHostEnvironment env, string loadAssembliesPath = null)

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.

So the only thing that can be done with the registrar is to dispose it?

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.

Correct. This is an internal method that is only invoked on that command-line. It creates an object that listens for new assemblies to be loaded, and registers them automatically.

{
Contracts.CheckValue(env, nameof(env));

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will attempt to register lots of assemblies (e.g. all framework assemblies). Isn't it wasteful?

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.

Good point. I brought back the code that checks if the assembly references the assembly containing the LoadableClassAttributeBase class.

boolfound=false;
vartargetName=target.GetName();
foreach(varnameinassembly.GetReferencedAssemblies())
{
if(name.Name==targetName.Name)
{
found=true;
break;
}
}
if(!found)
continue;

}

LoadAssembliesInDir(env, dir, false);
}

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.

Who deletes the temp path and when?

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 guess nobody. This is an internal API that is only called from 2 places: HelpCommand and ResultProcessor, so it isn't part of our public API after this change.

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated

foreach (var s in _filePrefixesToAvoid)
{
if (name.StartsWith(s))

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 think you should use the invariant culture/comparison

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.

Good call. I changed this to be StringComparison.OrdinalIgnoreCase

string name = Path.GetFileName(path).ToLowerInvariant();
switch (name)
{
case "cqo.dll":

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.

How did we come up with the list? Are we should the list does not have items that are not needed anymore? Might be at least worth to add a comment.

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 the list in the current code. I just moved the list out of the ComponentCatalog class.

privatestaticboolShouldSkipPath(stringpath)
{
stringname=Path.GetFileName(path).ToLowerInvariant();
switch(name)
{
case"cqo.dll":
case"fasttreenative.dll":
case"libiomp5md.dll":
case"libvw.dll":
case"matrixinterf.dll":
case"microsoft.ml.neuralnetworks.gpucuda.dll":
case"mklimports.dll":
case"microsoft.research.controls.decisiontrees.dll":
case"microsoft.ml.neuralnetworks.sse.dll":
case"neuraltreeevaluator.dll":
case"optimizationbuilderdotnet.dll":
case"parallelcommunicator.dll":
case"microsoft.ml.runtime.runtests.dll":
case"scopecompiler.dll":
case"tbb.dll":
case"internallearnscope.dll":
case"unmanagedlib.dll":
case"vcclient.dll":
case"libxgboost.dll":
case"zedgraph.dll":
case"__scopecodegen__.dll":
case"cosmosClientApi.dll":

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
var paths = Directory.EnumerateFiles(dir, "*.dll");
foreach (string path in paths)
{
if (filter && ShouldSkipPath(path))

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 think we should at least log/trace when we skip files. If somebody creates an extension that starts with "Clr" (not very far fetched) it will silently fail to load.

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've logged an Info message for this.

private static void LoadAssembliesInDir(IHostEnvironment env, string dir, bool filter)
{
if (!Directory.Exists(dir))
return;

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 do you silently return? Why not throw or at least debug assert.

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.

Because there are cases where this is called on potentially non-existing directories, for example here where we try calling it on the AutoLoad folder, which may not exist. (again that was all existing behavior that I'm refactoring out of ComponentCatalog.)

Comment threadsrc/Common/AssemblyLoadingUtils.cs Outdated
env.ComponentCatalog.RegisterAssembly(assembly);
return assembly;
}
catch (Exception)

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.

Similarly, why do we try to swallow all these errors?

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've changed it to only swallow errors from Assembly.LoadFrom, and log an error, which is the existing behavior.

verWeCanReadBack: 0x00010001,
loaderSignature: LoaderSignature);
loaderSignature: LoaderSignature,
loaderAssemblyName: typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typeof(MultiOutputRegressionPerInstanceEvaluator).Assembly.FullName [](start = 36, length = 67)

So, would it be at all possible for this to have, if left as default or null, for Assembly.GetCallingAssembly to be invoked, since every time this is constructed I think it should be in the same class and assembly as is calling this constructor?

Not sure if this is too dangerous with inlining.

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 did a small benchmark of using Assembly.GetExecutingAssembly, and it was considerably slower than using the typeof approach, which is why I opt'ed to go with the typeof approach.

I didn't consider the Assembly.GetCallingAssembly, but it could possibly work in the default cases, but I'm not sure if we should rely on it all over. My thinking was that it is best to be explicit.

I can make the change if you feel strongly about it.

@TomFinleyTomFinleySep 24, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, I don't know, what would have been a change in a handful of files has instead changed to a change in a hundred plus files, because of this choice. So would it not be better to not do this?

Also regarding timings, generally this sort of thing (serialization or deserialization) does not happen in a tight loop so I'm not too worried about it. How slow is it, really?


In reply to: 219941373 [](ancestors = 219941373)

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.

From the msdn Remarks section

If the method that calls the GetCallingAssembly method is expanded inline by the just-in-time (JIT) compiler, or if its caller is expanded inline, the assembly that is returned by GetCallingAssembly may differ unexpectedly.

I think explicitly specifying the assembly name is probably the most correct thing to do here.

private const uint VerWrittenCur = 0x00010001;
//private const uint VerWrittenCur = 0x00010001; // Initial
private const uint VerWrittenCur = 0x00010002; // Added AssemblyName
private const uint VerReadableCur = 0x00010001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0x00010001 [](start = 44, length = 10)

Hi @eerhardt, this looks mostly good thank you.

I have tested out your change locally, and noticed that when I try to load a new model in the old code, it fails with invalid format. It should fail with a more informative message that it is an invalid version, and I suspect the reason why it does not is you did not update this.

VerReadableCur should be bumped, if you did not intend the new format to be readable by the old software. If you did intend the new format to still be readable by the old software, you I have I think a bug somewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hypothetically though you could allow this, if you had written the assembly name in such a way that it did not change the format of the stream itself -- perhaps either by putting it into the unused portion of the header, or else in a side stream of the model (that is, another tiny file in the same directory in the zip archive, that will only be read and looked for if the loadname is unrecognized). This might be a nice property. While forwards compatibility is not a goal, neither do we go out of our way to absolutely guarantee that old versions can't read the new formats.

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.

Unfortunately, there isn't room in the unused portion of the header as the assembly names are rather long strings.

There are non-repository scenarios to worry about right? I don't have access to the Repository in the TryLoadModelCore method, where it tries to create the instance from the ComponentCatalog.

For now, I've bumped the VerReadableCur value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well all right. It might have been nice to do but whatever, forwards compatibility I guess isn't a primary goal. We've just never broken it so starkly as we have here. 😄 But perhaps it's warranted.

@eerhardt

Copy link
Copy Markdown
MemberAuthor

Thanks for the review. I've responded to all the current feedback.

@TomFinleyTomFinley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @eerhardt !

@codemzscodemzs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:shipit:

@eerhardt
eerhardt merged commit a02807c into dotnet:masterSep 25, 2018
@eerhardt
eerhardt deleted the ComponentCatalogRefactor branch September 25, 2018 16:38
@TomFinleyTomFinley mentioned this pull request Feb 28, 2019
4 tasks
@ghostghost locked as resolved and limited conversation to collaborators Mar 28, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@eerhardt@justinormont@codemzs@TomFinley@KrzysztofCwalina