Skip to content

Conversation

robieta
Copy link
Contributor

@robieta robieta commented Apr 26, 2018

This is a mock up of what arg parsing would look like with absl.flags instead of argparse. Run demo.py to see -help and -helpfull. Sadly there is no -h.

EDIT: -h is possible.

@robieta robieta requested review from karmel and qlzh727 April 26, 2018 20:45
@robieta robieta requested a review from a team as a code owner April 26, 2018 20:45
# limitations under the License.
# ==============================================================================

import absl.flags
Copy link
Member

Choose a reason for hiding this comment

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

I would do from absl import flags, so that all the code below can do flag.DEFINE_xxx. This will make the copybara rule easy, and also work with internal code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems reasonable.

def define_base(*args, **kwargs):
key_flags = _base.define_base(*args, **kwargs)
[absl.flags.declare_key_flag(fl) for fl in key_flags]

Copy link
Member

Choose a reason for hiding this comment

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

Why do we have this extra level of define? Can't we just mark the flags to be key flag when they are declared?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If declared when flags.DEFINE_* is called then they are key flags only for that module. However the declaration needs to be in core.py in order for the adopt_module_key_flags() chaining to work correctly. DEFINEs are magical context-aware little beasts.

import absl.app
import absl.flags

from official.utils.flags import core
Copy link
Member

Choose a reason for hiding this comment

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

I would put an alias here to core_flags, so that the core.define_base() becomes core_flags.define_base(), which provides more context about core package.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do.

# limitations under the License.
# ==============================================================================

import absl.app
Copy link
Member

Choose a reason for hiding this comment

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

same, from absl import app.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am less sold on this one. Other things could conceivably want the namespace app.

Copy link
Contributor

Choose a reason for hiding this comment

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

as absl_app? Moot point if this will be removed.

@@ -0,0 +1,102 @@
# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder: this will all have to be added to BUILD files.


help_wrap = functools.partial(flags.text_wrap, length=80, indent=" ",
firstline_indent="\n")

Copy link
Contributor

Choose a reason for hiding this comment

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

All magic must be extensively commented.


if export_dir:
flags.DEFINE_string(
name="export_dir", short_name="ed", default=None,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be in base now, as of #4041

to_choices_str(DTYPE_MAP.keys()))))

@flags.validator(flag_name="dtype", message="Valid dtypes: {}"
.format(to_choices_str(DTYPE_MAP.keys())))
Copy link
Contributor

Choose a reason for hiding this comment

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

Ack. This is way too much to pack into a single decorator call. At least extract the string formatting.

"for fp16 is 128 and 1 for all other dtypes."))

@flags.validator(flag_name="loss_scale", message="loss_scale should be a "
"positive integer.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto. extract message to _loss_scale_msg or similar, for clarity.


def define_performance(*args, **kwargs):
key_flags = _performance.define_performance(*args, **kwargs)
[flags.declare_key_flag(fl) for fl in key_flags]
Copy link
Contributor

Choose a reason for hiding this comment

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

We will want an instruction set somewhere-- if I want to add a new flag, what do I do, and what decorators/key_flags/etc. does it require? Similarly, for adding a new group of flags, and removing a flag or group.

# limitations under the License.
# ==============================================================================

import absl.app
Copy link
Contributor

Choose a reason for hiding this comment

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

as absl_app? Moot point if this will be removed.

"p": "ProfilerHook",
"eps": "ExamplesPerSecondHook",
"lm": "LoggingMetricHook",
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we relying more on abbreviations here? This makes me nervous, because it's easy to imagine someone adding a new flag-- say, --parse, -p, and then having sort out later why things break when the loggers are added too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is something I've wanted for a while, but the formatting in argparse forbade a good help message for it. I added an "h" to the end of them all, so ProfilerHook is "ph" instead of "p" to make it a little harder to mess up.

"A comma separated list of (case insensitive) strings to specify "
"the names of training hooks.\n{}\n\u180E "
"Example: `--hooks ProfilerHook,ExamplesPerSecondHook`\n\u180E "
"(or) `-hk p,eps`\nSee official.utils.logs.hooks_helper for "
Copy link
Contributor

Choose a reason for hiding this comment

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

What's going on with formatting here? Intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Intentional. I go into more detail in flags/_example.py, but basically it is needed for pretty help messages.

# Construct a pretty summary of hooks.
pad_len = max([len(i) for i in hooks_helper.HOOKS_ALIAS.values()]) + 6
hook_list_str = (
"\u180E {}Abbreviation\n".format("Hook".ljust(pad_len)) + "\n".join(
Copy link
Contributor

Choose a reason for hiding this comment

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

I dislike having to encode unicode chars here. Are normal characters insufficient? A dash or asterisk wouldn't suffice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I share your trepidation, but I've tested in P2 and P3. There sadly isn't a good ASCII solution.

@robieta robieta force-pushed the absl_flag_proposal branch 2 times, most recently from a5d829c to 09a74f5 Compare April 30, 2018 18:09
@robieta robieta changed the title WIP: Strawman for modular absl usage Move argparsing from builtin argparse to absl Apr 30, 2018
@robieta
Copy link
Contributor Author

robieta commented Apr 30, 2018

@karmel @qlzh727 This is ready for a second pass.

Copy link
Contributor

@karmel karmel left a comment

Choose a reason for hiding this comment

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

High-level question: should we be using tf.flags to keep consistent with tf proper? https://github.com/tensorflow/tensorflow/blob/r1.8/tensorflow/python/platform/flags.py

])


@flags_core.call_only_once
Copy link
Contributor

Choose a reason for hiding this comment

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

Nooooo. You can't use absl AND arg parsing magic.

Copy link
Member

Choose a reason for hiding this comment

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

second it. If we do this just because of test case, then we are doing something wrong in test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Grumble grumble. Actually the setupClass approach is much better. I'm happy to ditch this decorator.


if flags.multi_gpu:
validate_batch_size_for_multi_gpu(flags.batch_size)
if FLAGS.multi_gpu:
Copy link
Contributor

Choose a reason for hiding this comment

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

One benefit of having the flags passed in is that it's easier to use/reuse main by passing in an arbitrary object. Thoughts on keeping this as main(flags) and passing in FLAGS in main?

Copy link
Member

Choose a reason for hiding this comment

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

The FLAGS value can actually be overwritten, eg by test class. I was lying about the immutability for the flag values within FLAGS instance. The one in JAVA is immutable, and the python one is mutable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially I directly used the global because of the need to make calls to things like get_tf_dtype() for values which aren't stored directly in the FLAGS object, and it seemed wrong to take a parameter and then deeper in call out to a global. I think I'm just going to make all conversion functions pure functions to address that.

I'm still very much of the opinion that a global parameter object is something that is tolerated rather than desired, so having a clear and early conversion into more normal OOP is very appealing.

# Automatically determine device and data_format
(device, data_format) = ('/gpu:0', 'channels_first')
if flags.no_gpu or tfe.num_gpus() <= 0:
if flags.FLAGS.no_gpu or tfe.num_gpus() <= 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why FLAGS in mnist, but flags.FLAGS here?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, should just be consistent and stay with FLAGS.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moot now.

def _check_version(v):
return v in choices

rs_choice_str = ""
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Different quotations marks used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gone.


rs_choice_str = ""
if resnet_size_choices is not None:
rs_choice_str = '\n' + flags_core.to_choices_str(resnet_size_choices)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this just to add a newline I would prefer not having a newline and removing 4 lines of argparsing code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gone.

from __future__ import division
from __future__ import print_function

from absl import flags
Copy link
Contributor

Choose a reason for hiding this comment

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

This is cute, but I would prefer a guide (md) that starts with use cases and then gives examples. As a developer, I want a simple "Here's how to add a new flag, here's how to add a new set of flags, here's how to validate a flag," rather than having to find and parse a separate module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please don't call my work cute. I've added a readme that outlines the basics of using absl, although I don't want to replicate too much of absl's docs.

def define_mnist_eager_flags():
"""Defined flags and defaults for MNIST in eager mode."""
flags_core.define_base_eager()
flags_core.define_image()
Copy link
Contributor

Choose a reason for hiding this comment

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

image_model might be more clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it is less clear. (Since we're not actually defining any part of the model itself.)

)
@flags.validator('version', message='Invalid ResNet version.')
def _check_version(v):
return v in choices
Copy link
Contributor

Choose a reason for hiding this comment

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

Does absl really not validate natively that the chosen choice is in choices? That is surprising.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using enum.

"""Defined flags and defaults for MNIST in eager mode."""
flags_core.define_base_eager()
flags_core.define_image()
flags.adopt_module_key_flags(flags_core)
Copy link
Contributor

Choose a reason for hiding this comment

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

I've read through flags_core, and I still don't understand what this is doing. What does this do, and can we somehow make that activity more clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes arguments show up in -help instead of just -helpfull. I think there is just going to have to be some familiarizing with absl concepts like key flags.


if __name__ == "__main__":
define_flags()
absl_app.run(main)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be included in docs potentially, but not as a first-class module.


if flags.multi_gpu:
validate_batch_size_for_multi_gpu(flags.batch_size)
if FLAGS.multi_gpu:
Copy link
Member

Choose a reason for hiding this comment

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

The FLAGS value can actually be overwritten, eg by test class. I was lying about the immutability for the flag values within FLAGS instance. The one in JAVA is immutable, and the python one is mutable.

])


@flags_core.call_only_once
Copy link
Member

Choose a reason for hiding this comment

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

second it. If we do this just because of test case, then we are doing something wrong in test.

# Automatically determine device and data_format
(device, data_format) = ('/gpu:0', 'channels_first')
if flags.no_gpu or tfe.num_gpus() <= 0:
if flags.FLAGS.no_gpu or tfe.num_gpus() <= 0:
Copy link
Member

Choose a reason for hiding this comment

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

Agreed, should just be consistent and stay with FLAGS.


input_function = flags.use_synthetic_data and get_synth_input_fn() or input_fn
def main(_):
input_function = (flags.FLAGS.use_synthetic_data and get_synth_input_fn()
Copy link
Member

Choose a reason for hiding this comment

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

Same, please use FLAGS instead of flags.FLAGS.

Copy link
Member

Choose a reason for hiding this comment

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

please fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moot.

self._test_cifar10model_shape(version=2)

def test_cifar10_end_to_end_synthetic_v1(self):
cifar10_main.define_cifar_flags()
Copy link
Member

Choose a reason for hiding this comment

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

Since we are not statically defining flags, we will then need a static setup function in the test case to cover that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.



def to_choices_str(choices):
return "(choices: {})".format(", ".join([str(i) for i in choices]))
Copy link
Member

Choose a reason for hiding this comment

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

The enum flag can support this I think


if data_format:
choices = ["channels_first", "channels_last"]
flags.DEFINE_string(
Copy link
Member

Choose a reason for hiding this comment

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

Just use the enum flag and validation is come by default.



def get_tf_dtype():
return DTYPE_MAP[flags.FLAGS.dtype][0]
Copy link
Member

Choose a reason for hiding this comment

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

Please use FLAGS instead of flags.FLAGS.


def call_only_once(f):
"""Prevent unittests from defining flags multiple times."""
setattr(f, "already_called", False)
Copy link
Member

Choose a reason for hiding this comment

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

Please use the static setup function in test class to ensure the flags is defined only once.

class BaseTester(unittest.TestCase):

def setUp(self):
define_flags()
Copy link
Member

Choose a reason for hiding this comment

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

Should do as following:

@classmethod
def setUpClass(cls):
define_flags()

parser = MNISTArgParser()
flags = parser.parse_args(args=argv[1:])

def main(_):
Copy link
Member

Choose a reason for hiding this comment

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

instead of just _ here, which might confuse people, how about "unused_argv"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moot now.


input_function = flags.use_synthetic_data and get_synth_input_fn() or input_fn
def main(_):
input_function = (flags.FLAGS.use_synthetic_data and get_synth_input_fn()
Copy link
Member

Choose a reason for hiding this comment

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

please fix.

flags.FLAGS.set_default(name=key, value=value)


def parse_flags(argv=None):
Copy link
Member

Choose a reason for hiding this comment

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

This should be removed now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so. It's still used.

Copy link
Contributor Author

@robieta robieta left a comment

Choose a reason for hiding this comment

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

I have addressed the comments; however, I need to rewrite history (which will do who knows what to reviews on github) so I'm submitting this review now. I will make another comment when you should actually review it.

parser = MNISTArgParser()
flags = parser.parse_args(args=argv[1:])

def main(_):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moot now.


if flags.multi_gpu:
validate_batch_size_for_multi_gpu(flags.batch_size)
if FLAGS.multi_gpu:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially I directly used the global because of the need to make calls to things like get_tf_dtype() for values which aren't stored directly in the FLAGS object, and it seemed wrong to take a parameter and then deeper in call out to a global. I think I'm just going to make all conversion functions pure functions to address that.

I'm still very much of the opinion that a global parameter object is something that is tolerated rather than desired, so having a clear and early conversion into more normal OOP is very appealing.

# Automatically determine device and data_format
(device, data_format) = ('/gpu:0', 'channels_first')
if flags.no_gpu or tfe.num_gpus() <= 0:
if flags.FLAGS.no_gpu or tfe.num_gpus() <= 0:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moot now.

def define_mnist_eager_flags():
"""Defined flags and defaults for MNIST in eager mode."""
flags_core.define_base_eager()
flags_core.define_image()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it is less clear. (Since we're not actually defining any part of the model itself.)

"""Defined flags and defaults for MNIST in eager mode."""
flags_core.define_base_eager()
flags_core.define_image()
flags.adopt_module_key_flags(flags_core)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes arguments show up in -help instead of just -helpfull. I think there is just going to have to be some familiarizing with absl concepts like key flags.

from __future__ import division
from __future__ import print_function

from absl import flags
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please don't call my work cute. I've added a readme that outlines the basics of using absl, although I don't want to replicate too much of absl's docs.

flags.FLAGS.set_default(name=key, value=value)


def parse_flags(argv=None):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so. It's still used.



def parse_flags(argv=None):
"""Reset flags and reparse. Currently only used in testing."""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I put it here because there isn't a utils/testing/misc.py, and this doesn't seem important enough to make one. (Plus we could end up using it somewhere else.)

[__file__, "--dtype", dtype_str, "--loss_scale", "5"])

assert args.loss_scale == 5
assert flags_core.get_loss_scale() == 5
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should be. Fixed.

"ph": "ProfilerHook",
"epsh": "ExamplesPerSecondHook",
"lmh": "LoggingMetricHook",
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright. They're gone.

@robieta robieta force-pushed the absl_flag_proposal branch from ec3bd95 to 638530f Compare May 2, 2018 18:06
@robieta robieta force-pushed the absl_flag_proposal branch from 90d69af to f53c230 Compare May 2, 2018 22:37
Args:
flags: FLAGS object that contains the params for running. See
ResnetArgParser for created flags.
flags_obj: An object containing parsed flags.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "See ... for details."


## Key Flags
absl has the concept of a key flag. Any flag defined in `__main__` is considered a key flag by
default. Key flags are displayed in `--help`, others only appear in `--helpfull`. Obviously this
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it is not obvious that this is insufficient, or what precisely you mean here. Maybe you mean, "In order to handle key flags that are defined outside the module in question, absl provides..."

pro-tip: almost nothing is obvious when you're this deep into TF and other magical libraries.


when `my_module.py` is run it will show the help text for `my_flag`. Because not all flags defined
in a file are equally important, `official/utils/flags/core.py` provides an abstraction for handling
key flag declaration in an easy way.
Copy link
Contributor

Choose a reason for hiding this comment

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

Namely? Confusing to mention but not show. Also, linking directly to the code in question is helpful.

```

Validators take the form that returning True (truthy) passes, and all others
(False, None, exception) fail.
Copy link
Contributor

Choose a reason for hiding this comment

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

Also: We provide common flags in .... To use these, call ... in your module.
And: In order to add a test for a module declaring flags, you must...

# line after flags are listed.
help_wrap = functools.partial(flags.text_wrap, length=80, indent="",
firstline_indent="\n")

Copy link
Contributor

Choose a reason for hiding this comment

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

Currently the only major effect? Is it expected that will change over time?

And, if this is staying, please doc it. How does one use this? When should one use this?

@robieta
Copy link
Contributor Author

robieta commented May 2, 2018

Addressed.

@robieta robieta merged commit 5f9f6b8 into master May 3, 2018
@robieta robieta deleted the absl_flag_proposal branch May 3, 2018 00:25
omegafragger pushed a commit to omegafragger/models that referenced this pull request May 15, 2018
* squash of modular absl usage commits

* delint

* address PR comments

* change hooks to comma separated list, as absl behavior for space separated lists is not as expected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants