1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! # LogSoftmax
//!
//! Computes log(softmax(x)) along specified axis.
//!
//! **ONNX Spec**: <https://onnx.ai/onnx/operators/onnx__LogSoftmax.html>
//!
//! ## Opset Versions
//! - **Opset 1**: Initial version with LogSoftmax operation.
//! - **Opset 11**: Changed default axis from 1 to -1 (last dimension); clarified axis behavior.
//! - **Opset 13**: Added bfloat16 type support; no functional changes to operation semantics.
//!
//! **Implementation Note**: This implementation validates opset 13+.
//!
//! ## Missing Test Coverage
//! - TODO: No test for axis=0 or positive axis values - Only axis=-1 tested
//! - TODO: No test for negative axis normalization edge cases - e.g., axis=-rank should map to 0
//! - TODO: No test for 1D tensors - Simplest case not tested
//! - TODO: No test for higher-rank tensors (4D, 5D) - Only 2D tested
//! - TODO: No test validating numerical stability with extreme values (very large/small inputs)
//! - TODO: No test for all-zero or constant inputs - Edge cases for softmax normalization
//! - TODO: No test validating that input must be floating-point type - Integer inputs should be rejected
use crate::ir::{ArgType, Argument, Node, RawNode};
use crate::processor::{
InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec, ProcessError,
};
use derive_new::new;
use onnx_ir_derive::NodeBuilder;
/// Configuration for LogSoftmax operations
#[derive(Debug, Clone, new)]
pub struct LogSoftmaxConfig {
/// Axis along which to apply log softmax
pub axis: usize,
}
/// Node representation for LogSoftmax operation
#[derive(Debug, Clone, NodeBuilder)]
pub struct LogSoftmaxNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: LogSoftmaxConfig,
}
pub(crate) struct LogSoftmaxProcessor;
impl NodeProcessor for LogSoftmaxProcessor {
type Config = LogSoftmaxConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 1,
max_opset: None,
inputs: InputSpec::Exact(1),
outputs: OutputSpec::Exact(1),
}
}
fn infer_types(
&self,
node: &mut RawNode,
_opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
// TODO: Validate input tensor dtype is floating-point type - Type constraint not enforced - burn/crates/onnx-ir/src/node/log_softmax.rs:54
// TODO: Validate unexpected attributes before config extraction
// The spec only supports "axis" attribute
for (key, _value) in node.attrs.iter() {
match key.as_str() {
"axis" => {}
_ => {
return Err(ProcessError::InvalidAttribute {
name: key.clone(),
reason: format!("Unexpected attribute for LogSoftmax: {}", key),
});
}
}
}
// Infer output type
crate::processor::same_as_input(node);
Ok(())
}
fn extract_config(&self, node: &RawNode, opset: usize) -> Result<Self::Config, ProcessError> {
// Extract the shape of the input tensor
let tensor = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => tensor.clone(),
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor".to_string(),
actual: format!("{:?}", node.inputs.first().unwrap().ty),
});
}
};
// Axis default changed between opset versions:
// opset 1-12: default axis = 1 (with 2D coercion semantics)
// opset 13+: default axis = -1 (direct axis operation)
let mut axis: i64 = if opset < 13 { 1 } else { -1 };
for (key, value) in node.attrs.iter() {
if key.as_str() == "axis" {
axis = value.clone().into_i64()
}
}
// if axis is negative, it is counted from the end
if axis < 0 {
axis += tensor.rank as i64;
}
// TODO: Validate converted axis is within bounds [0, rank) - Out of bounds axis should be rejected - burn/crates/onnx-ir/src/node/log_softmax.rs:103
let config = LogSoftmaxConfig {
axis: axis as usize,
};
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::LogSoftmax(LogSoftmaxNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::NodeType;
use crate::node::test_utils::TestNodeBuilder;
fn create_test_node(axis: i64, input_rank: usize) -> RawNode {
TestNodeBuilder::new(NodeType::LogSoftmax, "test_log_softmax")
.input_tensor_f32("data", input_rank, None)
.output_tensor_f32("output", input_rank, None)
.attr_int("axis", axis)
.build()
}
#[test]
fn test_log_softmax_config_basic() {
let node = create_test_node(-1, 3);
let mut node = node;
let processor = LogSoftmaxProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 2); // -1 + 3 = 2 (last dimension)
}
#[test]
fn test_log_softmax_config_explicit_axis() {
let node = create_test_node(1, 3);
let mut node = node;
let processor = LogSoftmaxProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 1);
}
#[test]
fn test_log_softmax_config_multiple_inputs() {
let mut node = create_test_node(1, 3);
// Add an extra input
let extra_input = TestNodeBuilder::new(NodeType::Identity, "temp")
.input_tensor_f32("extra", 1, None)
.build()
.inputs
.pop()
.unwrap();
node.inputs.push(extra_input);
let processor = LogSoftmaxProcessor;
let spec = processor.spec();
let result = crate::processor::validate_node_spec(&node, 16, &spec);
assert!(matches!(
result,
Err(ProcessError::InvalidInputCount {
expected: 1,
actual: 2
})
));
}
}