-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support dump with graphmodel.execute #6953
Conversation
b0a9784
to
900ad48
Compare
@qjia7 @xhcao @haoyunfeix @gyagp PTAL |
private tensorsPendingDisposalForAsync: NamedTensorsMap; | ||
private idsKeepForAsync: Set<number>; | ||
// Variables with Sync suffix is used for dumping by execute. | ||
private tensorsPendingDisposalForSync: Tensor[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you unify to use tensorsPendingDisposal
or intermediateTensorsPendingDisposal
for both async and sync for simplicity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
private tensorsMap: NamedTensorsMap; | ||
private keepTensorForDebug = false; | ||
private dumpMode = DumpMode.Default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A boolean variable keepTensorForDebug
like before is enough? I think the tidy
won't be used for async execute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Default mode, it does dispose; Sync mode, does nothing; Async mode: pushes tensor to disposal queue.
if (this.dumpMode === DumpMode.Default) {
tensor.dispose();
} else if (this.dumpMode === DumpMode.Async) {
this.tensorsPendingDisposal.push(tensor);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In sync mode, it won't go to this code since tensor.kept = true
since you add keepTensors
before it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. But I think putting code under "(this.dumpMode === DumpMode.Async)" help to remind this is dump specific.
BTW, I move this logic outside this checkTensorForDisposal in the latest change.
@@ -45,10 +52,11 @@ export class GraphExecutor implements FunctionExecutor { | |||
private _functions: {[key: string]: Graph} = {}; | |||
private _functionExecutorMap: {[key: string]: FunctionExecutor} = {}; | |||
private _resourceManager: ResourceManager; | |||
private intermediateTensors: NamedTensorsMap = {}; | |||
private keepIds: Set<number>; | |||
// Variables with Async suffix is used for dumping by executeAsync. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Update the annotation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
Object.keys(inputs).forEach(name => { | ||
const [nodeName, index] = parseNodeName(name); | ||
const tensors: Tensor[] = []; | ||
tensors[index] = inputs[name]; | ||
tensorsMap[nodeName] = tensors; | ||
// Input tensors should be disposed by user. | ||
this.keepTensors(tensors); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we need to add keepTensors
for inputs compared with the original code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add some comments above this line:
// For some models, such as bodypix, it will dispose the input tensors
// in its top level tidy. In dump mode, these tensors are required, so
// call keep to eusure they are preserved. However, this comes with a
// side effect in dump mode, tensor leak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use this.keepTensors(tensors, this.tensorsPendingDisposal);
to avoid tensor leak?
if (this.keepIntermediateTensors)
{
this.keepTensors(tensors, this.tensorsPendingDisposal);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leak only happens in dump mode. And 'tensors' includes some tensors will be used in later inference, especially in e2e, which will run inference on two different backends.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, I think we need to fix it. We need to find which place releases it and debug why it's released and then reused again in debug mode. Or give reason that why tensor leak is unavoidable.
if (this.dumpMode === DumpMode.Sync) { | ||
this.tensorsMap = tensorsMap; | ||
} else { | ||
this.tensorsMap = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's safe to say this.tensorsMap = tensorsMap
since L280 make sure that async won't go to this path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. This 'if' help to remind this is dump specific. Under non-dump mode, this.tensorsMap will never be used.
private tensorsMap: NamedTensorsMap; | ||
private keepTensorForDebug = false; | ||
private dumpMode = DumpMode.Default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In sync mode, it won't go to this code since tensor.kept = true
since you add keepTensors
before it?
this.tensorsPendingDisposal = null; | ||
} | ||
if (this.dumpMode === DumpMode.Async) { | ||
this.disposeTensorsMap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why we need to call this.disposeTensorsMap()
only for async mode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intended behaviour:
In sync mode, intermediat tensors are kept in tensorsPendingDisposal.
In async mode, intermediat tensors = = tensormap - tensorsToKeep.
This is why we have two different dispose.
Please note in the updated version, I tried to align these two path, which means, for both mode, all intermediat tensors are kept in tensorsPendingDisposal.
0128d75
to
a6949d3
Compare
|
||
Object.keys(inputs).forEach(name => { | ||
const [nodeName, index] = parseNodeName(name); | ||
const tensors: Tensor[] = []; | ||
tensors[index] = inputs[name]; | ||
tensorsMap[nodeName] = tensors; | ||
// Input tensors should be disposed by user. | ||
this.keepTensors(tensors); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use this.keepTensors(tensors, this.tensorsPendingDisposal);
to avoid tensor leak?
if (this.keepIntermediateTensors)
{
this.keepTensors(tensors, this.tensorsPendingDisposal);
}
private tensorsMap: NamedTensorsMap; | ||
private keepTensorForDebug = false; | ||
private keepTensorsForDump = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: For me, keepIntermediateTensors
is a better name since the value is set by KEEP_INTERMEDIATE_TENSORS
. And the purpose of it is not only for dump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Xing.
The code looks good in my side. But the tensor leak issue still needs you to do more investigation to figure out the reason.
Will add more reviewers. Thanks.
|
||
Object.keys(inputs).forEach(name => { | ||
const [nodeName, index] = parseNodeName(name); | ||
const tensors: Tensor[] = []; | ||
tensors[index] = inputs[name]; | ||
tensorsMap[nodeName] = tensors; | ||
// Input tensors should be disposed by user. | ||
this.keepTensors(tensors); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, I think we need to fix it. We need to find which place releases it and debug why it's released and then reused again in debug mode. Or give reason that why tensor leak is unavoidable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 1 approvals obtained (waiting on @axinging, @jinjingforever, and @qjia7)
tfjs-converter/src/executor/graph_executor.ts
line 193 at r6 (raw file):
} private keepTensors(
this assumes there will be no duplication of tensors from the tensorsToKeep array, might be better to change tensorsPendingDisposal array to a set to enable auto-dedupe.
tfjs-converter/src/executor/graph_executor.ts
line 268 at r6 (raw file):
Previously, qjia7 (Jiajia Qin) wrote…
Anyway, I think we need to fix it. We need to find which place releases it and debug why it's released and then reused again in debug mode. Or give reason that why tensor leak is unavoidable.
I am against keeping the input tensors as intermediate tensor of graph model, since they are not generated inside the model. This is beyond the tfjs model dump, it is part of the model API pipeline dump.
tfjs-converter/src/executor/graph_executor.ts
line 308 at r6 (raw file):
this.intermediateTensors[nodeName][index] = tensor; } else { this.intermediateTensors[nodeName] = [];
the intermediateTensors contains the name for the tensors, how is the current approach tracking the name?
=> Done
=> Done. With this change, for bodypix, we can not get the whole intermediate tensors because the inputs are disposed.
=> The original intermediateTensors is a misnomer, it should be tensorsPendingDisposal. The API getIntermediateTensors returns this.tensorMap. So the name in original intermediateTensors is of no use. @pyu10055 ,updated, PTAL |
@pyu10055 @jinjingforever
From above code, we can see asBatch will be disposed after tidy returns. We have two options about this:
Our goal is to support dumping on more models. And this tensor leak happens only when dump is on. So I personally prefer keeping these input tensors. WDYT? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank @axinging for explaining, if I understand correctly, you are using the dump mode for the graph model to do an tensor audit for tfjs-models API that does not share a specific model file?
That sounds like quite different from the intermediate tensor dumping use case, still not clear in this case, why the inputs cannot be disposed?
Reviewable status: 0 of 1 approvals obtained (waiting on @jinjingforever and @qjia7)
About: "why the inputs cannot be disposed?" ?
Conclusion: for both graphModel.getIntermediateTensors and remove fake errors, all input tensors are required. Input tensors are used differently on e2e In current e2e, there are two kinds of input tensor use scenarios, examples are MobileNetV3 and bodypix.
In MobileNetV3, the input is never disposed at user side(This is a tensor leak). So the dump of MobileNetV3 is of full functionality. And keep this tensor in graph model is meanless for dump.
As mentioned before, input tensor asBatch will be disposed in tidy. If we do not keep it(call keep) in graph model, graphModel.getIntermediateTensors can not get all the required tensors, and remove fake errors is incomplete. (This means dump on bodypix is incomplete) Based on the above MobileNetV3&bodypix, to have better dump dupport, we'd better keep the input tensors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@axinging Thanks, this makes a lot more sense now, thanks. One nitpick is the name - intermediate tensors is not very intuitive, probably better be called 'inferenceTensorAudit', which could contains inputs + intermediateTensors+outputs.
I think for both MobileNetV3 and bodypix, the input tensor should be disposed as the benchmark is completed.
So, given that the dump has tracked for all tensors, when caller request dispose those tensors, it should be able to remove them all?
Why there will be memory leak?
Reviewable status: 0 of 1 approvals obtained (waiting on @jinjingforever and @qjia7)
about: Why there will be memory leak?We can understand this by models: e2e/MobileNetv3, bodypix. e2e/MobileNetv3 : tensor leak can be fixed.
In above code, 'input' will never be disposed, this is a tensor leak (This exists long before, un-relating to dump feature). I think this a potential bug and this can be fixed by call input.dispose after predict is done (I will try to work out a fix for this later). bodypix: no tensor leak or un-dumpable.
To support bodypix dump or avoid tensor leak at dump mode, we have three options at below line: Option 1: do not keep input tensors.
Option 2: Keep input tensors, not add it to the this.tensorsPendingDisposal.
Option 3: Keep input tensors, add it to the this.tensorsPendingDisposal.
We need consider e2e/MobileNetv3 together with e2e/bodypix here. Because they have different useage of input. The basic workflow of current e2e conformance test:
About name inferenceTensorAuditI prefer a name like "GraphModel.getNamedTensorsMap", because getIntermediateTensors returns NamedTensorsMap which contains all tensors:
BTW, if change name from getIntermediateTensors to getNamedTensorsMap, it seems not related to flag KEEP_INTERMEDIATE_TENSORS namely. But from logic, getNamedTensorsMap do return a NamedTensorsMap, and KEEP_INTERMEDIATE_TENSORS do keep all "INTERMEDIATE TENSORS". So summarize my proposal:
WDYT? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edited
I had a offline discussion with @mattsoulanille
We believe it would be better to use tensor.clone() to keep the input and intermediate tensors.
clone() would increase the ref count, while not forceful preventing the tensor to be disposed.
which means you need store the name and clone tensors instead just the name.
@mattsoulanille please feel free to chime in.
Reviewable status: 0 of 1 approvals obtained (waiting on @jinjingforever and @qjia7)
To add to Ping's discussion, I think we can use Instead of using this.tensorsMap = Object.fromEntries(Object.entries(tensorsMap).map(([name, tensorsList]) => {
const cloned = tensor.clone();
// This clone needs to be 'kept' because model.execute may be called within a tidy(). We don't want
// tidy() to dispose these cloned tensors because we need to look at them after the model has finished
// executing (after the 'tidy()').
//
// However, we don't check whether the tensor is 'kept' when we free it.
keep(clone);
return [name, tensorsList.map(tensor => tensor.clone())];
})); When a user calls When we need to clean up the intermediate tensors, we no longer need to iterate for (const tensorsList in Object.keys(this.tensorsMap)) {
for (const tensor of tensorsList) {
// This is a clone of the real tensor (i.e. another reference), so it's okay to dispose it.
// We're not disposing the user's input tensor here. Just the clone.
tensor.dispose();
}
} If the user calls Works when a tensor is reusedSuppose the user reuses an input tensor like this: const input = tf.randomNormal([1, 224, 224, 3]);
function runModel() {
return model.execute(input);
} We can count the number of references to the underlying data of the
Works when a new tensor is created inside a
|
8894c6f
to
926c46e
Compare
Thanks @pyu10055 @mattsoulanille. The updated change is with keep+clone, PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @axinging, the PR LGTM with some minor questions.
Reviewable status: 0 of 1 approvals obtained (waiting on @axinging, @jinjingforever, and @qjia7)
tfjs-converter/src/executor/graph_executor.ts
line 193 at r6 (raw file):
Previously, axinging (Xu Xing) wrote…
=> Done
just want to confirm that inputs are included in the tensorsMap, thanks
tfjs-converter/src/executor/graph_executor.ts
line 560 at r13 (raw file):
const currentContext = context.currentContext; if (util.isPromise(tensors)) { if (this.keepIntermediateTensors) {
why keepIntermediateTensors would not work with promise?
These promise should be resolved to tensors and put into the tensorMap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 3 files at r1, 1 of 2 files at r14, 1 of 1 files at r15, all commit messages.
Reviewable status: complete! 1 of 1 approvals obtained (waiting on @jinjingforever and @qjia7)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with one nit.
@@ -256,6 +276,10 @@ export class GraphExecutor implements FunctionExecutor { | |||
if (this.parent == null) { | |||
context.dispose(tensorsToKeep); | |||
} | |||
if (this.keepIntermediateTensors) { | |||
this.clonedTensorsMap = this.cloneTensorMap(tensorsMap); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should this if
happen before L276?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks.
@pyu10055 @mattsoulanille @jinjingforever Currently, getIntermediateTensors()
will return all cloned tensors in the graph, which including inputs, weights, intermediate tensors, outputs. Maybe we need a new meaningful name for it in future. Just list here to get your attention!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to take a look at this PR in a few minutes, but I'm sending this review now to prevent it from being merged before I can take a look (since It already has two approvals).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for waiting for my review!
@@ -645,7 +651,7 @@ export class GraphExecutor implements FunctionExecutor { | |||
private mapInputs(inputs: NamedTensorMap) { | |||
const result: NamedTensorMap = {}; | |||
for (const inputName in inputs) { | |||
const tensor = this._signature?.inputs?.[inputName]; | |||
const tensor = this._signature ?.inputs ?.[inputName]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const tensor = this._signature ?.inputs ?.[inputName]; | |
const tensor = this._signature?.inputs?.[inputName]; |
@@ -669,7 +675,7 @@ export class GraphExecutor implements FunctionExecutor { | |||
|
|||
private mapOutputs(outputs: string[]) { | |||
return outputs.map(name => { | |||
const tensor = this._signature?.outputs?.[name]; | |||
const tensor = this._signature ?.outputs ?.[name]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const tensor = this._signature ?.outputs ?.[name]; | |
const tensor = this._signature?.outputs?.[name]; |
Object.entries(this.clonedTensorsMap).forEach(([, tensorsList]) => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.isDisposed) { | ||
tensor.dispose(); | ||
} | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic can probably be factored out into another function, since it's used in _executeAsync
as well.
Object.entries(this.clonedTensorsMap).forEach(([, tensorsList]) => { | |
tensorsList.forEach(tensor => { | |
if (tensor && !tensor.isDisposed) { | |
tensor.dispose(); | |
} | |
}); | |
}); | |
// Put this function outside of the class. | |
function tensorMapForEach(tensorsMap: NamedTensorsMap, f: (tensor: Tensor) => void) { | |
for (const tensorsList in Object.values(tensorsMap) { | |
for (const tensor in tensorsList) { | |
f(tensor); | |
} | |
} | |
} | |
tensorMapForEach(this.clonedTensorsMap, tensor => { | |
if (tensor && !tensor.isDisposed) { | |
tensor.dispose(); | |
} | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can do this in a follow up change? In fact I donot see the benefit of this refactor, and the original version is more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have made this a Nit suggestion. We can keep the current implementation as-is.
Object.entries(tensorsMap).forEach(([, tensorsList]) => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.kept && !tensor.isDisposed && | ||
!keepIds.has(tensor.id)) { | ||
tensor.dispose(); | ||
} | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I factored iterating over a tensor map into another function.
Object.entries(tensorsMap).forEach(([, tensorsList]) => { | |
tensorsList.forEach(tensor => { | |
if (tensor && !tensor.kept && !tensor.isDisposed && | |
!keepIds.has(tensor.id)) { | |
tensor.dispose(); | |
} | |
}); | |
}); | |
tensorMapForEach(tensorsMap, tensor => { | |
if (tensor && !tensor.kept && !tensor.isDisposed && | |
!keepIds.has(tensor.id)) { | |
tensor.dispose(); | |
} | |
}); |
if (this.keepIntermediateTensors) { | ||
this.clonedTensorsMap = this.cloneTensorMap(tensorsMap); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should dispose all the original intermediate tensors in tensorsMap
now that we've cloned them. Otherwise, we're leaking them.
Does it make more sense to clone each tensor in the above for loop? That way, we can immediately dispose it if necessary (in this.checkTensorForDisposal()
). That also simplifies this.checkTensorForDisposal
, which would no longer need to check this.keepIntermediateTensors
, because the tensor is already cloned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is possible. But 1), this.cloneTensorMap can not be reused, we need some new logic to do this.
2), Your above mentioned tensorMapForEach refactor conflicts with this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Matt is right. For the original intermediate tensors, we should clone each of them when we get them in L274. Otherwise, we are leaking them due to the changes (L330) you add in this.checkTensorForDisposal
. Since you already use cloned tensor, theoretically, this.checkTensorForDisposal
should be unchanged. And the code logic you mentioned can be changed based on our requirement.
Similar for the async execute.
} | ||
|
||
Object.entries(tensorsMap).forEach(([, tensorsList]) => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.kept && !tensor.isDisposed && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this check tensor.kept
? I think that's mostly reserved for tidy
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact I am not clear why kept is checked here, this is from the original version: https://github.com/tensorflow/tfjs/pull/5659/files#diff-44e0a825cd7c6f31c03d9333db5dc21a8937d66a5b28d719c699863eef96ad8dL369.
I keep it because I am not clear about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is this was from the previous logic that used kept
to save the intermediate tensors, although I could be wrong.
It looks like the keepIds
set is what prevents this from disposing the input, output, and weight tensors, so we're not using tensor.kept
to prevent them from being disposed. If all the other tensors in tensorsMap
are intermediate tensors, then I think it's safe to remove this check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
6cb4acd
to
1c7fedf
Compare
Thanks @mattsoulanille for your great feedback. 2), tensorMapForEach:
Your suggestion:
3), The return value of "private cloneTensorMap(tensorsMap: NamedTensorsMap) {" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making the changes I requested! Here are a few more, and then I think this will be ready to merge.
|
||
Object.keys(inputs).forEach(name => { | ||
const [nodeName, index] = parseNodeName(name); | ||
const tensors: Tensor[] = []; | ||
tensors[index] = inputs[name]; | ||
tensorsMap[nodeName] = tensors; | ||
this.cloneTensorList(tensors, nodeName, this.clonedTensorsMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Make this.cloneTensorsList
return a cloned list of tensors instead of storing them in a map. I think this is a bit easier to read, and it matches this.cloneTensorsMap
. Also, then this.cloneTensorsMap
can use this.cloneTensorsList
in its implementation.
this.cloneTensorList(tensors, nodeName, this.clonedTensorsMap); | |
this.clonedTensorsMap[nodeName] = this.cloneTensorList(tensors); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -247,15 +290,18 @@ export class GraphExecutor implements FunctionExecutor { | |||
`Please use model.executeAsync() instead.`); | |||
} | |||
tensorsMap[node.name] = tensors; | |||
this.cloneTensorList(tensors, node.name, this.clonedTensorsMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need a check for this.keepIntermediateTensors
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it doesn't, because this.clonedTensorsMap
is null, but IMO that's a bit confusing. See my other comment on this.
|
||
Object.keys(inputs).forEach(name => { | ||
const [nodeName, index] = parseNodeName(name); | ||
const tensors: Tensor[] = []; | ||
tensors[index] = inputs[name]; | ||
tensorsMap[nodeName] = tensors; | ||
this.cloneTensorList(tensors, nodeName, this.clonedTensorsMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tensors should only be cloned if this.keepIntermediateTensors
is set, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see, cloneTensorsList
checks if this.clonedTensorsMap
is null before running. I find that a bit confusing. Can we do the check here instead?
Also, I think it might be better for this.cloneTensorsList
to return a list of cloned tensors instead of mutating the tensors map. See my nit.
Object.values(this.clonedTensorsMap).forEach(tensorsList => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.isDisposed) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Google's TS style guide prefers for (... of ...)
instead of .forEach(...)
(.map(...)
is still okay when using the resulting value).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
|
||
Object.entries(tensorsMap).forEach(([, tensorsList]) => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.kept && !tensor.isDisposed && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is this was from the previous logic that used kept
to save the intermediate tensors, although I could be wrong.
It looks like the keepIds
set is what prevents this from disposing the input, output, and weight tensors, so we're not using tensor.kept
to prevent them from being disposed. If all the other tensors in tensorsMap
are intermediate tensors, then I think it's safe to remove this check.
} | ||
|
||
private cloneTensorList( | ||
tensors: Tensor[], nodeName: string, tensorsMap: NamedTensorsMap) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tensors: Tensor[], nodeName: string, tensorsMap: NamedTensorsMap) { | |
tensors?: Tensor[], nodeName?: string, tensorsMap?: NamedTensorsMap) { |
We'd like to enable strictNullChecks in the future, so if you don't accept the nit that changes this function to return a list of tensors instead of mutating tensorsMap
, please mark all these arguments as optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return a list of tensors
Currently only graph model predicting with executeAsync supports dump. This has two drawbackes: 1. Some models don't support dump. 2. For model has a wrapping layer over grapp model, such as bodypix, pose-detection, a lot of change is required to support dump, example change: tensorflow/tfjs-models#841. This change removes this limitation, so that more models are supported and dump is easier.
Co-authored-by: Matthew Soulanille <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattsoulanille PTAL
} | ||
|
||
private cloneTensorList( | ||
tensors: Tensor[], nodeName: string, tensorsMap: NamedTensorsMap) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return a list of tensors
|
||
Object.keys(inputs).forEach(name => { | ||
const [nodeName, index] = parseNodeName(name); | ||
const tensors: Tensor[] = []; | ||
tensors[index] = inputs[name]; | ||
tensorsMap[nodeName] = tensors; | ||
this.cloneTensorList(tensors, nodeName, this.clonedTensorsMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Object.values(this.clonedTensorsMap).forEach(tensorsList => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.isDisposed) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
|
||
Object.entries(tensorsMap).forEach(([, tensorsList]) => { | ||
tensorsList.forEach(tensor => { | ||
if (tensor && !tensor.kept && !tensor.isDisposed && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks!
Currently only graph model predicting with executeAsync supports dump. This has two drawbackes:
This change removes these limitations and enables dumping on more models.
To see the logs from the Cloud Build CI, please join either our discussion or announcement mailing list.
This change is