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

Enable some Jinja extensions and add datetime capabilities #32684

Merged
merged 14 commits into from
Aug 23, 2024

Conversation

Rocketknight1
Copy link
Member

@Rocketknight1 Rocketknight1 commented Aug 14, 2024

This PR enables a few built-in Jinja features, specifically the loopcontrols extension. This allows a few new features in templates, most notably the ability to break or continue in loops, which was previously impossible.

Also, this PR adds the ability to get the current date/time with a call to strftime(), since this was requested by the Llama team. A new test_jinja_extensions test is added to check that these features work as intended.

@HuggingFaceDocBuilderDev

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Copy link
Collaborator

@amyeroberts amyeroberts left a comment

Choose a reason for hiding this comment

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

Thanks for adding!

This PR is mixing two different things which should be tested separately. Once that's done I think it'll be good to go

Comment on lines 1933 to 1934
def strftime(format):
return datetime.now().strftime(format)
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's strftime but only for the now datetime object. It would be better to do something like def strftime_now(format)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

@@ -1153,6 +1153,39 @@ def test_chat_template_batched(self):
dummy_conversations, chat_template=dummy_template, tokenize=True
) # Check that no error raised

@require_jinja
def test_jinja_extensions(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test name is far too generic! Is it testing extensions can be used? testing specific extensions? which extensions?

Copy link
Member Author

@Rocketknight1 Rocketknight1 Aug 15, 2024

Choose a reason for hiding this comment

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

Renamed to test_jinja_extensions_are_enabled

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, even better, split it into two tests with clearer names!

Copy link
Collaborator

@amyeroberts amyeroberts left a comment

Choose a reason for hiding this comment

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

Thanks for updating!

In general, I don't think adding strftime_now is a good idea - it's a highly specific functionality which isn't very flexible. We should push against adding lots of little disconnected features.

Now the tests have been decoupled - this PR should really be too (the extensions and datetime are unrelated)

Comment on lines 1933 to 1935
def strftime_now(format):
return datetime.now().strftime(format)

Copy link
Collaborator

Choose a reason for hiding this comment

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

We shouldn't add functionalities like this into core tokenization code -- enabling certain things in a jinja template has nothing to do with tokenization and we risk bloating the module with many unrelated tools. Instead, I'd propose two things:

  • We move strftime_now to its own module we import from e.g. jinja_template_utils or jinja_template_tools or [insert better name]. This would be a library where we can define all these different utilities. raise_exception should go there too.
  • We make the jinja template more extensible, such that we don't have to hard code this for everyone. It's been requested - but why expose this utility for all template users? It won't be a sustainable way to grow and manage this. Instead we could have e.g. a jinja_globals mapping which is used to add each named function e.g.
jinja_globals = {
    "raise_exception": raise_exception,
    "strftime_now": strftime_now
}

and then have model-specific defaults for their own tokenizers / templates, if we want.

The second suggestion I feel less strong about - very open to other suggestions on how to avoid adding all this logic inside the tokenizers

Copy link
Member Author

@Rocketknight1 Rocketknight1 Aug 15, 2024

Choose a reason for hiding this comment

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

Hmmn, moving it out of the tokenizer I agree with! However, I think I disagree with the model-specific jinja_globals mapping. The reason is that we'll have to hard-code everything users need as a Python function somewhere. We won't actually be able to let users add new functions with their model repo, because these extensions are arbitrary Python code and would require trust_remote_code=True.

Therefore, since we have to hardcode everything in the transformers codebase anyway, requiring specific extensions to be enabled in a model-specific jinja_globals adds confusion and extra model maintenance burden for no real gain - the only difference when an extension is enabled is that certain functions or keywords become usable inside Jinja.

Therefore, I think we should just enable all of the extensions we support all the time. I don't think there is any performance penalty, so the only reason to disable an extension would be if users want to use a variable name that clashes with an extension, and we want to discourage that anyway.

This might change in future if we have a "dangerous" extension that poses a security risk, and so we want to limit it only to models that absolutely require it, to avoid a malicious template being able to call it. I have no plans to do anything like that right now, though, so I think that feature can wait until it's needed, if it ever is.

self.assertEqual(break_output, "system 1") # Loop should break after first iter

@require_jinja
def test_jinja_strftime(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice - far clearer and cleaner tests!

@Rocketknight1
Copy link
Member Author

@amyeroberts good advice on the code - I moved it all out of the base tokenizer and into chat_template_utils, which cleans up the base tokenizer a lot. I think keeping strftime_now is a good idea, though, for three reasons:

  1. It fulfills a need that will be very common (templates want to get the current date/time as a string for their system message)
  2. stftime() is flexible enough for a lot of formats
  3. We can't really expose more complex classes/libraries/datetime objects in Jinja to cater for all possible needs, only callable functions like this. As single functions go, this is a pretty useful/general one!

Copy link
Collaborator

@amyeroberts amyeroberts left a comment

Choose a reason for hiding this comment

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

Thanks for iterating on this!

I disagree on how we should manage the extensions, but there aren't too many at the moment.

It will become increasingly difficult to manage if more of these get added. Different teams and model owners will have different requirements: we either need to maintain an extremely clean set of atomic functionalities which can be composed or risk a mess of overlapping functionalities with no clear way for users to know the best approach for performing certain tasks with the templates.

@Rocketknight1
Copy link
Member Author

@amyeroberts Yeah, I understand! The problem is that the templates have to run in Python-jinja2, huggingface/jinja for JS, and Minijinja in Rust/TGI. This means I can't, for example, have composable functions that return datetime objects from the Python library, because those objects won't exist in the other languages!

That means I really do have to stick to functions that operate over simple types, and build for common use cases rather than universality. I think strftime_now is a decent compromise there. Also, we're not necessarily locked-in - if it turns out in future that we made a mistake, we can deprecate the function and search the Hub for templates using it and make automatic PRs to do it a different way.

@Rocketknight1 Rocketknight1 merged commit 371b9c1 into main Aug 23, 2024
24 checks passed
@Rocketknight1 Rocketknight1 deleted the jinja_new_features branch August 23, 2024 13:26
@amyeroberts
Copy link
Collaborator

This means I can't, for example, have composable functions that return datetime objects from the Python library, because those objects won't exist in the other languages!

That's kind of my point! As we can't build a clean library of functions we should try to avoid adding functionality globally as it can't be well abstracted.

we can deprecate the function and search the Hub for templates using it and make automatic PRs to do it a different way.

Deprecation is hard and best avoided whenever possible. Searching on the hub is also no sufficient for catching everything -- people will have code running locally which we cannot automatically update. We've experiencing the pain of this in another situation with deprecation of some params in image processor configs, which we can never fully complete, so I'd rather we avoid this whenever possible

@Rocketknight1
Copy link
Member Author

This is fair - I'll try to keep it to a minimum! Still, something like strftime_now() is necessary for a lot of templates, just because "Current Date: 23 Aug 2024" or something like that is an increasingly common component of standard system messages.

zucchini-nlp pushed a commit to zucchini-nlp/transformers that referenced this pull request Aug 30, 2024
…ce#32684)

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Fix strftime template

* Add template strip() just to be safe

* Remove the do extension to make porting easier, and also because it's the least useful

* Rename test

* strftime -> strftime_now

* Split test

* Update test to use strftime_now

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils
zucchini-nlp pushed a commit to zucchini-nlp/transformers that referenced this pull request Aug 30, 2024
…ce#32684)

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Fix strftime template

* Add template strip() just to be safe

* Remove the do extension to make porting easier, and also because it's the least useful

* Rename test

* strftime -> strftime_now

* Split test

* Update test to use strftime_now

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils
itazap pushed a commit to NielsRogge/transformers that referenced this pull request Sep 20, 2024
…ce#32684)

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Fix strftime template

* Add template strip() just to be safe

* Remove the do extension to make porting easier, and also because it's the least useful

* Rename test

* strftime -> strftime_now

* Split test

* Update test to use strftime_now

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils
BernardZach pushed a commit to BernardZach/transformers that referenced this pull request Dec 5, 2024
…ce#32684)

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Fix strftime template

* Add template strip() just to be safe

* Remove the do extension to make porting easier, and also because it's the least useful

* Rename test

* strftime -> strftime_now

* Split test

* Update test to use strftime_now

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils
BernardZach pushed a commit to innovationcore/transformers that referenced this pull request Dec 6, 2024
…ce#32684)

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Add new Jinja features:

- Do extension
- Break/continue in loops
- Call strftime to get current datetime in any format

* Fix strftime template

* Add template strip() just to be safe

* Remove the do extension to make porting easier, and also because it's the least useful

* Rename test

* strftime -> strftime_now

* Split test

* Update test to use strftime_now

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils

* Refactor everything out into chat_template_utils
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.

3 participants