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

Add Operation Instances for accessing graph edges during graph construction #360

Merged
merged 1 commit into from
May 16, 2022

Conversation

Corallus-Caninus
Copy link
Contributor

@Corallus-Caninus Corallus-Caninus commented Apr 19, 2022

Expose Output types for each Operation as mentioned in issue #358 and create an object we can add features to such as Input types for runtime (graph build time) access of Operation properties.

@google-cla
Copy link

google-cla bot commented Apr 19, 2022

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

For more information, open the CLA check for this pull request.

@Corallus-Caninus
Copy link
Contributor Author

signed the CLA let me know what else I have to do this is my first Google contrib.

@Corallus-Caninus Corallus-Caninus changed the title Master Expose named operations as a builder alternative Apr 19, 2022
@Corallus-Caninus
Copy link
Contributor Author

Need to rename commit message from odd to add, I'll do this after work. Also maybe this should be a struct with Operation and generated Outputs with names instead of a tuple with HashMap of which I'm not a fan.

@dskkato
Copy link
Contributor

dskkato commented Apr 19, 2022

Need to rename commit message from odd to add,

In this case, you can use git commit --amend and modify the message. Then, git push --force to update your repository's branch.

Copy link
Contributor

@adamcrume adamcrume left a comment

Choose a reason for hiding this comment

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

This does look like a very useful improvement, but I think we could make it a bit better. Rather than returning a map of strings to outputs, it probably makes more sense to return a new type for every op. For example, for the Batch operation, it would make sense to do:

impl Batch {
    pub fn build_outputs<O0: ::std::convert::Into<crate::Output>>(
        &self,
        in_tensors: O0,
        scope: &mut crate::Scope,
    ) -> crate::Result<BatchOutputs> {
        ...
    }
}

pub struct BatchOutputs {
  operation: crate::Operation,
}

impl BatchOutputs {
    pub fn operation(&self) -> &crate::Operation {
        &self.operation
    }

    pub fn batched_tensors(&self) -> crate::Output {
        crate::Output {
            operation: self.operation.clone(),
            index: 0,
        }
    }

    pub fn batch_index(&self) -> crate::Output {
        crate::Output {
            operation: self.operation.clone(),
            index: 1,
        }
    }

    pub fn id(&self) -> crate::Output {
        crate::Output {
            operation: self.operation.clone(),
            index: 2,
        }
    }
}

This means that users would be able to do:

let batch = Batch::new().build_outputs(...)?;
let batched_tensors: Output = batch.batched_tensors();

rather than:

let (batch_op, batch_outputs) = Batch::new().build_outputs(...)?;
let batched_tensors: Output = batch_outputs.get(&"batched_tensors".to_string()).ok_or(some_error())?.clone();

In other words, generating actual methods rather than using a map is much clearer, automatically generates better docs, and means that users don't have to worry about misspelling keys or error handling for missing map entries.

@@ -221,6 +226,98 @@ fn write_attr<W: Write>(w: &mut W, attr: &Attr) -> Result<(), io::Error> {
Ok(())
}

//Corallus-Caninus: same as above but return a HashMap of output names to Outputs so the
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need to include your name in the comments.

Copy link
Contributor Author

@Corallus-Caninus Corallus-Caninus Apr 20, 2022

Choose a reason for hiding this comment

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

I just wrote something like this let me update the PR.

Edit: my commit is slightly different. It still uses a HashMap but with getters, I can look into a codegen solution but I'm still learning the low level bindgen to the pbtxt.

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 please tell me any other formatting standards I'll remove the comment names.

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 I can make this happen give me awhile to chew on this.

@Corallus-Caninus
Copy link
Contributor Author

alright I had some difficulties with git but please let me know what you think of the most recent commit I pushed thanks.

@Corallus-Caninus Corallus-Caninus changed the title Expose named operations as a builder alternative Expose Outputs for built Operations Apr 25, 2022
Copy link
Contributor

@adamcrume adamcrume left a comment

Choose a reason for hiding this comment

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

Moving in the right direction!

src/graph.rs Outdated
@@ -1059,6 +1069,7 @@ impl Operation {

/// Returns the type of a specific input.
pub fn input_type(&self, index: usize) -> DataType {
/// and the return value is the source operation and the index into its output array.
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this supposed to go above in the docs for output?

src/graph.rs Outdated
Ok(result)
}
}
impl ::core::ops::Index<i64> for OutputSlice {
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency with the rest of the codebase, this should use std::ops::Index at the top of the file and then impl Index<i64> here.

src/graph.rs Outdated
@@ -1750,6 +1761,98 @@ impl Output {

////////////////////////

/// An OutputSlice allows slicing an variable length Operation to retrieve its outputs.
#[derive(Debug, Clone)]
pub struct OutputSlice {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any particular reason to use this rather than Vec<Output> and a function to create them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nope, patching now.

write!(w, "impl {}Outputs {{\n", op_name)?;

for (i, output) in outputs.iter().enumerate() {
if output.number_attr.is_some() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of calling unwrap() below, this should do:

if let Some(number_attr) = &output.number_attr {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

patching now.

output.rust_name
)?;
//create an Output for this index
write!(w, " let forward_output = crate::Output {{\n")?;
Copy link
Contributor

Choose a reason for hiding this comment

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

forward_output doesn't need to be created at all. Instead of forward_output.operation.clone() below, it can just use self.op.clone().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes thank you I have fixed this.

//now get that Outputs operation and create an OutputSlice given the runtime length
write!(
w,
r#" crate::graph::OutputSlice::new(forward_output.operation.clone(), self.op.get_attr_int("{}").unwrap())"#,
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of unwrap(), this function should return Result<Vec<Output>>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

affirmative.

) -> Result<(), io::Error> {
let mut escaper = Escaper::new(keywords);
let escaped_args: Vec<_> = args.iter().map(|arg| escaper.escape(&arg)).collect();
write!(w, " fn build_impl_outputs(&self, ")?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be called build_outputs_impl.

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 so too.

write!(w, " }}\n")?;
for attr in attrs {
write_set_attr(w, attr, &node_var)?;
//TODO: also get type and value of each number_attr by looking up the corresponding attribute
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 intended to be done in this PR or later?

//now get that Outputs operation and create an OutputSlice given the runtime length
write!(
w,
r#" crate::graph::OutputSlice::new(forward_output.operation.clone(), self.op.get_attr_int("{}").unwrap())"#,
Copy link
Contributor

Choose a reason for hiding this comment

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

These are always starting at index 0, but that's not correct if this is not the first output arg.

Also, for both this and the else branch, the index needs to be adjusted for the size of prior outputs. In other words, if the first output_arg has size 4, and the second has size 5, then the first needs to take outputs 0-3 and the second needs to take 4-8.

To do this, you'll probably need to read all the relevant int attributes as per the TODO above and either pass the Outputs or the indices into the FooOutputs struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thank you, patching this now.

number_attr_opt = Some(number_attr.clone());
}
op_outputs.push(Output {
//NOTE: we should be able to reuse attr keywords, correct me if im wrong.
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 probably use a new Escaper. Reusing attr_escaper means that if an op has an attr named foo and an output named foo then the generated code would be BlahOutputs::foo_() (i.e. with an underscore), even though they shouldn't actually clash.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

patching now, Ill also do this for Inputs in the next PR

@Corallus-Caninus
Copy link
Contributor Author

Corallus-Caninus commented Apr 29, 2022

Thank you for the code review. I will be attending to these errors as well as the following:

So looking at the .pbtxt, I think I need to expose inputs too or we will end up doing this eventually. I am thinking about restructuring this PR to create operation specific structs and leaving the builder functions as generic operation types that return these "inherited" operation specific structs for inputs and outputs at least as a starting point. This would also go in another module (eventually) since the code is becoming spaghetti with this PR feature. I've updated this PRs name to reflect this objective.

@Corallus-Caninus Corallus-Caninus changed the title Expose Outputs for built Operations Add Operation Instances for accessing graph edges during graph construction May 1, 2022
Copy link
Contributor

@adamcrume adamcrume left a comment

Choose a reason for hiding this comment

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

Hm, I don't know why the format checker refuses to run. Please run cargo fmt on src/ops/ops_impl.rs.

Just a few smallish changes, and then I think this will be good to merge.

op_name: &str,
attrs: &[Attr],
args: &[String],
outputs: Vec<Output>,
Copy link
Contributor

Choose a reason for hiding this comment

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

This function doesn't use attrs, args, or outputs.

//counts holds the number of times an entry is repeated in offset
let counts = dynamic_offset
.iter()
.fold(HashMap::new(), |mut counts, String| {
Copy link
Contributor

Choose a reason for hiding this comment

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

String should be lower case, since it's a variable.

" let dynamic_offset = ({}) as i32;\n",
scalar_offsets
)?;
write!(w, " let mut Outputs = vec![];\n",)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Outputs should be lower case since it's a variable.

if dynamic_offset.is_empty() {
write!(
w,
" for i in {}..self.op.get_attr_int(\"{}\")? as i32{{\n",
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of \"{}\", it's safer to use {:?} for these, because it will automatically escape any special characters.

write!(w, "}}\n")?;

Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be simplified a bit, but I can hack on it after the PR is merged.

let mut number_attr_opt = None;
if !number_attr.is_empty() {
number_attr_opt = Some(number_attr.clone());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These five lines can be done in one go:

let number_attr_opt = if output.number_attr.is_empty() {
  None
} else {
  Some(output.number_attr.clone())
};

which also reduces the amount of cloning.

@Corallus-Caninus
Copy link
Contributor Author

Corallus-Caninus commented May 13, 2022

alright Ive followed up with your review and promised myself I wouldn't add any more features until I open a new PR. Thanks again for your help @adamcrume .

@adamcrume adamcrume merged commit 110b139 into tensorflow:master May 16, 2022
@adamcrume
Copy link
Contributor

Thanks! This should be useful for a number of things.

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