Repository: onnx/onnx

Issue:
Referencing function attributes in body
### Question
Is there currently a standard way (as described in the [IR](https://github.com/onnx/onnx/blob/main/docs/IR.md)) to reference function attributes inside the function body (i.e. the nodes)?

### Further information
- Relevant Area: IR, model definition, functions

### Notes

The internal implementation uses the field `ref_attr_name` (for example used [here](https://github.com/onnx/onnx/blob/b064f0a88cbe2fb75081dc8d25adde942ad9a916/onnx/defs/parser.cc#L386), called from e.g. `MeanVarianceNormalization`'s [implementation](https://github.com/onnx/onnx/blob/64e79b78a410e69a804f4f198bbaf45193fb3c32/onnx/defs/nn/defs.cc#L2181)) in the language parser, but this does not seem to be documented in the standard nor exposed in `onnx.helper`.

I've found a workaround for accessing function attributes when defining a function in Python: Directly use the protobuf interface for creating an `AttributeProto`:
```py
def ref_attr(name: str, ref_name: str, value_type: onnx.AttributeProto.AttributeType):
    attr = onnx.AttributeProto()
    attr.name = name
    attr.ref_attr_name = ref_name
    attr.type = value_type
    return attr
```

This can be used like so, explicitly adding an `AttributeProto` to `NodeProto.attribute`:
```py
fun_shift_node = onnx.helper.make_node(
    'BitShift',
    ['A', 'L'],
    ['B']
)
# onnx.helper does not support referencing function attributes, do it manually (to specify direction):
fun_shift_node.attribute.append(ref_attr("direction", "dir", onnx.AttributeProto.STRING))

fun = onnx.helper.make_function(
    'test',
    'FunBitShift',
    ['A', 'L'],
    ['B'],
    nodes=[fun_shift_node],
    opset_imports=[onnx.helper.make_operatorsetid('', 16)],
    attributes=["dir"]
)  # fun<dir>: (A, L) -> B
```

Then we can specify the attribute when calling the function normally:
```py
call_shift_node = onnx.helper.make_node(
    'FunBitShift',
    ['D', 'K'],
    ['R'],
    domain='test',
    dir="LEFT"
)  # R = FunBitShift<dir="LEFT">(D, K)
```

I found no alternate solution that avoids doing this outside the helper.



Discussion:
Yes, I believe you are correct. I think the helper is missing a utility to create a reference_attribute. It would be useful to add a utility function, say, `make_attribute_ref`. Your code seems fine, on a quick look.
If you are trying to create a single model, you can also try using the parser and parser-syntax. https://github.com/onnx/onnx/blob/4e24b635c940801555bee574b4eb3a34cab9acd5/onnx/parser.py#L10 is the API to invoke the parser. You can find examples here: https://github.com/onnx/onnx/blob/main/onnx/test/cpp/parser_test.cc and the syntax definition here: https://github.com/onnx/onnx/blob/main/docs/Syntax.md#the-syntax
In that case this also seems like a documentation bug, since I don't think this is mentioned in the IR (I did find it [kind of documented](https://github.com/onnx/onnx/blob/84f6ff689da8ec930877478d3a50fe5081ad582f/onnx/onnx.proto3#L140-L144) in `.proto`, however).
At the moment I'm just using the workaround `ref_attr` (which with some modification could just be added to `onnx.helper`). I think this should also be documented and supported in `onnx.helper` to avoid confusion.
I could try and set up a PR to correct `IR.md` and `helper.py`?
It's worth noting that the existing `onnx.helper.make_attribute` only performs inference for the `AttributeType` based on the passed in `value`:

https://github.com/onnx/onnx/blob/84f6ff689da8ec930877478d3a50fe5081ad582f/onnx/helper.py#L472-L476

If we wanted to support similar inference from a Python type (passing a literal `int`, `float`, ...) in `make_attribute_ref` - which could be quite useful - this would also require implementing a similar routine.
>> I could try and set up a PR to correct IR.md and helper.py?

That would be great, thanks!

>> If we wanted to support similar inference from a Python type (passing a literal int, float, ...)

In the contexts where we create an attribute_ref, it is unlikely we have an existing value to pass in. It seems to me passing in the type explicitly as a parameter would be fine?
> That would be great, thanks!

I'll add that to my todo-list in that case!

> It seems to me passing in the type explicitly as a parameter would be fine?

Yeah, that's exactly what I meant. In that case we might end up implementing something very close to what `make_attribute` does with values. It should be doable.

Resolving patch:
diff --git a/onnx/helper.py b/onnx/helper.py
--- a/onnx/helper.py
+++ b/onnx/helper.py
@@ -545,7 +545,23 @@ def make_attribute(
     return attr
 
 
+def make_attribute_ref(
+        name: str,
+        attr_type: AttributeProto.AttributeType,
+        doc_string: Optional[str] = None
+) -> AttributeProto:
+    """Make an AttributeProto holding a reference to the parent function's attribute of given name and type."""
+    attr = AttributeProto()
+    attr.name = name
+    attr.type = attr_type
+    if doc_string:
+        attr.doc_string = doc_string
+    return attr
+
+
 def get_attribute_value(attr: AttributeProto) -> Any:
+    if attr.ref_attr_name:
+        raise ValueError(f"Cannot get value of reference attribute: {attr}")
     if attr.type == AttributeProto.FLOAT:
         return attr.f
     if attr.type == AttributeProto.INT:


Test patch: