Skip to content
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

Avoid importing tensorflow when importing evaluate #135

Merged
merged 5 commits into from
Jul 18, 2022

Conversation

NouamaneTazi
Copy link
Member

@NouamaneTazi NouamaneTazi commented Jun 12, 2022

Avoid loading TFPreTrainedModel when it's not used because tensorflow allocates all GPU memory

Fixes the issue:

import evaluate # this would allocate all GPU memory

Note: also made a fix on pipeline to work as expected huggingface/transformers#17684

@HuggingFaceDocBuilderDev
Copy link

HuggingFaceDocBuilderDev commented Jun 12, 2022

The documentation is not available anymore as the PR was closed or merged.

Copy link
Contributor

@ola13 ola13 left a comment

Choose a reason for hiding this comment

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

Looks good, thanks @NouamaneTazi! How did you test the change?

@lvwerra lvwerra requested review from lhoestq and sgugger June 13, 2022 07:54
Copy link
Member

@lhoestq lhoestq left a comment

Choose a reason for hiding this comment

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

I think this won't work for users-defined models that inherit from PreTrainedModel.

I would simply import PreTrainedModel /TFPreTrainedModel directly here inside compute instead of importing it at the top of the file.

Calling compute is expensive anyway, so importing tensorflow is ok I think.

(alternatively you can check if tensorflow has already been imported by checking "tensorflow" in sys.modules AFAIK - if it's not been imported you don't need to import TFPreTrainedModel and check if the model inherits from TFPreTrainedModel)

@NouamaneTazi
Copy link
Member Author

Using the fix in this PR, importing evaluate should no longer allocate all the GPU memory.

And by using the PR huggingface/transformers#17684 as well, e.compute() should no longer allocate all the GPU memory neither.

from evaluate import evaluator
from datasets import Dataset, load_dataset

e = evaluator("text-classification")
data = Dataset.from_dict(load_dataset("imdb")["test"][:2])

# testing that nothing breaks
# from transformers import TFBertModel, BertModel
# tfmodel = TFBertModel.from_pretrained("julien-c/bert-xsmall-dummy")
# model = BertModel.from_pretrained("julien-c/bert-xsmall-dummy")

results = e.compute(
    model_or_pipeline="huggingface/prunebert-base-uncased-6-finepruned-w-distil-mnli",
    data=data,
    metric="accuracy",
    input_column="text",
    label_column="label",
    label_mapping={"LABEL_0": 0.0, "LABEL_1": 1.0},
    strategy="bootstrap",
    n_resamples=10,
    random_state=0
)

@NouamaneTazi
Copy link
Member Author

I'm not sure if "tensorflow" in sys.modules is the way to do it. Because if the user uses pipeline, that means they don't know in advance what's the model type right?

And I agree about the user-defined models @lhoestq

@NouamaneTazi
Copy link
Member Author

Testing script for user-defined model

import torch
from datasets import Dataset, load_dataset
from transformers import BertConfig, BertTokenizer, PreTrainedModel

from evaluate import evaluator


e = evaluator("text-classification")
data = Dataset.from_dict(load_dataset("imdb")["test"][:2])


class CustomModel(PreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
    def forward(self, *args, **kwargs):
        return {'logits': torch.zeros(1, 1, 1)}


model_or_pipeline = CustomModel(BertConfig.from_pretrained("julien-c/bert-xsmall-dummy"))

results = e.compute(
    model_or_pipeline=model_or_pipeline,
    tokenizer=BertTokenizer.from_pretrained("julien-c/bert-xsmall-dummy"),
    data=data,
    metric="accuracy",
    input_column="text",
    label_column="label",
    label_mapping={"LABEL_0": 0.0, "LABEL_1": 1.0},
    strategy="bootstrap",
    n_resamples=10,
    random_state=0,
)

Comment on lines 247 to 252
if isinstance(model_or_pipeline, str) or (
hasattr(model_or_pipeline, "__class__")
and any(
cls_name in [parent_cls.__name__ for parent_cls in model_or_pipeline.__class__.__mro__]
for cls_name in ["PreTrainedModel", "TFPreTrainedModel"]
)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if "tensorflow" in sys.modules is the way to do it. Because if the user uses pipeline, that means they don't know in advance what's the model type right?

If model_or_pipeline is a pipeline, you don't need to check if it inherits from TFPreTrainedModel, so you don't need to import TFPreTrainedModel ;)

I'll let @sgugger give his opinion, but I think this is reasonable:

Suggested change
if isinstance(model_or_pipeline, str) or (
hasattr(model_or_pipeline, "__class__")
and any(
cls_name in [parent_cls.__name__ for parent_cls in model_or_pipeline.__class__.__mro__]
for cls_name in ["PreTrainedModel", "TFPreTrainedModel"]
)
import transformers
if "tensorflow" in sys.modules:
# Check if the model if a TF model only if TF has already been imported.
# Indeed loading `TFPreTrainedModel` may import tensorflow unnecessarily.
transformers_pretrained_model_classes = (transformers.PreTrainedModel, transformers.TFPreTrainedModel)
else:
transformers_pretrained_model_classes = (transformers.PreTrainedModel)
if (
isinstance(model_or_pipeline, transformers_pretrained_model_classes)
or isinstance(model_or_pipeline, str)

Copy link
Member Author

Choose a reason for hiding this comment

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

After testing this, it seems that after this line from datasets import Dataset, load_dataset, "tensorflow" in sys.modules becomes True. And so it doesn't help with the problem.
Is it just my python?

Copy link
Member

Choose a reason for hiding this comment

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

Yea it's an issue with datasets, let me fix it

Copy link
Member

Choose a reason for hiding this comment

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

Actually it comes from huggingface_hub - let me open a PR

Copy link
Member

@lhoestq lhoestq Jun 13, 2022

Choose a reason for hiding this comment

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

It's already fixed on the main branch of huggingface-hub. I also added a tests here for the future: huggingface/huggingface_hub#904 and huggingface/datasets#4482

Copy link
Contributor

Choose a reason for hiding this comment

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

This suggestion might avoid the load yes. The current test as it is useless as no model is ever just a PreTrainedModel or a TFPreTrainedModel. They are always subclasses of those.

Copy link
Member

Choose a reason for hiding this comment

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

The current test as it is useless as no model is ever just a PreTrainedModel or a TFPreTrainedModel. They are always subclasses of those.

indeed, using isinstance is required here and it's fine to import PreTrainedModel (and also TFPreTrainedModel if tensorflow has been imported by the user) - see my suggestion above

Copy link
Contributor

Choose a reason for hiding this comment

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

I said: "This suggestion might avoid the load yes." ;-)
The rest of the comment is on the existing code.

Copy link
Member

@lhoestq lhoestq Jun 30, 2022

Choose a reason for hiding this comment

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

This is actually not enough to not import TF, since importing pipeline (from transformers.pipelines) does import TF. Is this expected ?

I wouldn't spend too much time in evaluate trying to not load TF at this point. Users can still provide USE_TF=0 to not import TF in transformers.

It's maybe a better solution to lazy import the evaluator module, so that transformers is not imported when evaluate is imported

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes pipeline imports TensorFlow if available to load the default model associated with each pipeline. I would recommend not importing pipeline before the time it's necessary.

@NouamaneTazi NouamaneTazi changed the title Avoid loading TFPreTrainedModel in evaluator.py Avoid importing tensorflow when importing evaluate Jun 16, 2022
@lvwerra lvwerra requested review from sgugger and removed request for sgugger June 23, 2022 12:45
Comment on lines 248 to 252
if (
isinstance(model_or_pipeline, PreTrainedModel)
or isinstance(model_or_pipeline, TFPreTrainedModel)
or isinstance(model_or_pipeline, str)
isinstance(model_or_pipeline, str)
or isinstance(model_or_pipeline, transformers.PreTrainedModel)
or isinstance(model_or_pipeline, transformers.TFPreTrainedModel)
):
Copy link
Member Author

Choose a reason for hiding this comment

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

After some more debugging, here's what I found:

import tensorflow # doesn't allocate all GPU memory
import transformers # doesn't allocate all GPU memory
from transformers import TFPreTrainedModel # allocates all GPU memory

So as long as we avoid using/importing transformers.TFPreTrainedModel, there shouldn't be a problem. Which is why I believe this is the simplest fix

@NouamaneTazi
Copy link
Member Author

Again, you can test that this is working by using

from evaluate import evaluator
from datasets import Dataset, load_dataset

e = evaluator("text-classification")
data = Dataset.from_dict(load_dataset("imdb")["test"][:2])

# testing that nothing breaks
# from transformers import TFBertModel, BertModel
# tfmodel = TFBertModel.from_pretrained("julien-c/bert-xsmall-dummy")
# model = BertModel.from_pretrained("julien-c/bert-xsmall-dummy")

results = e.compute(
    model_or_pipeline="huggingface/prunebert-base-uncased-6-finepruned-w-distil-mnli",
    data=data,
    metric="accuracy",
    input_column="text",
    label_column="label",
    label_mapping={"LABEL_0": 0.0, "LABEL_1": 1.0},
    strategy="bootstrap",
    n_resamples=10,
    random_state=0
)

And please note that calling evaluator.compute would call pipeline which would still call a TFPreTrainedModel and allocates all GPU memory. Which is why this PR is necessary

Copy link
Member

@lhoestq lhoestq left a comment

Choose a reason for hiding this comment

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

Sounds good to me, indeed it won't call transformers.TFPretrainedModel if the input is a string or a pytorch model :)

@lvwerra
Copy link
Member

lvwerra commented Jul 6, 2022

If @sgugger agrees, I think we can merge this.

@sgugger
Copy link
Contributor

sgugger commented Jul 6, 2022

Double-checked and indeed import pipeline does not allocate GPU memory while import TFPreTrainedModel does (which is a bug on Transformers probably, will dig more). So this should work (but ultimately be rendered unnecessary I hope!)

@sgugger
Copy link
Contributor

sgugger commented Jul 6, 2022

huggingface/transformers#18044 should fix the fact that importing TFPreTrainedModel takes all GPU memory in the Transformers side.

@NouamaneTazi
Copy link
Member Author

This PR is no longer necessary as the problem was solved from transformers side. But I think it's still better to avoid unnecessary imports, and to check instances in the order PT -> TF (just like it's done in transformers)

@lvwerra
Copy link
Member

lvwerra commented Jul 18, 2022

Still a good idea to have it so we don't need to pin a too new transformers version I think.

@lvwerra lvwerra merged commit 9b6cea3 into huggingface:main Jul 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants