scalar-autograd 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -2
- package/dist/CompiledFunctions.d.ts +111 -0
- package/dist/CompiledFunctions.js +268 -0
- package/dist/CompiledResiduals.d.ts +74 -0
- package/dist/CompiledResiduals.js +94 -0
- package/dist/EigenvalueHelpers.d.ts +14 -0
- package/dist/EigenvalueHelpers.js +93 -0
- package/dist/Geometry.d.ts +131 -0
- package/dist/Geometry.js +213 -0
- package/dist/GraphBuilder.d.ts +64 -0
- package/dist/GraphBuilder.js +237 -0
- package/dist/GraphCanonicalizerNoSort.d.ts +20 -0
- package/dist/GraphCanonicalizerNoSort.js +190 -0
- package/dist/GraphHashCanonicalizer.d.ts +46 -0
- package/dist/GraphHashCanonicalizer.js +220 -0
- package/dist/GraphSignature.d.ts +7 -0
- package/dist/GraphSignature.js +7 -0
- package/dist/KernelPool.d.ts +55 -0
- package/dist/KernelPool.js +124 -0
- package/dist/LBFGS.d.ts +84 -0
- package/dist/LBFGS.js +313 -0
- package/dist/LinearSolver.d.ts +69 -0
- package/dist/LinearSolver.js +213 -0
- package/dist/Losses.d.ts +9 -0
- package/dist/Losses.js +42 -37
- package/dist/Matrix3x3.d.ts +50 -0
- package/dist/Matrix3x3.js +146 -0
- package/dist/NonlinearLeastSquares.d.ts +33 -0
- package/dist/NonlinearLeastSquares.js +252 -0
- package/dist/Optimizers.d.ts +70 -14
- package/dist/Optimizers.js +42 -19
- package/dist/V.d.ts +0 -0
- package/dist/V.js +0 -0
- package/dist/Value.d.ts +84 -2
- package/dist/Value.js +296 -58
- package/dist/ValueActivation.js +10 -14
- package/dist/ValueArithmetic.d.ts +1 -0
- package/dist/ValueArithmetic.js +58 -50
- package/dist/ValueComparison.js +9 -13
- package/dist/ValueRegistry.d.ts +38 -0
- package/dist/ValueRegistry.js +88 -0
- package/dist/ValueTrig.js +14 -18
- package/dist/Vec2.d.ts +45 -0
- package/dist/Vec2.js +93 -0
- package/dist/Vec3.d.ts +78 -0
- package/dist/Vec3.js +169 -0
- package/dist/Vec4.d.ts +45 -0
- package/dist/Vec4.js +126 -0
- package/dist/__tests__/duplicate-inputs.test.js +33 -0
- package/dist/cli/gradient-gen.d.ts +19 -0
- package/dist/cli/gradient-gen.js +264 -0
- package/dist/compileIndirectKernel.d.ts +24 -0
- package/dist/compileIndirectKernel.js +148 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/scalar-autograd.d.ts +1157 -0
- package/dist/symbolic/AST.d.ts +113 -0
- package/dist/symbolic/AST.js +128 -0
- package/dist/symbolic/CodeGen.d.ts +35 -0
- package/dist/symbolic/CodeGen.js +280 -0
- package/dist/symbolic/Parser.d.ts +64 -0
- package/dist/symbolic/Parser.js +329 -0
- package/dist/symbolic/Simplify.d.ts +10 -0
- package/dist/symbolic/Simplify.js +244 -0
- package/dist/symbolic/SymbolicDiff.d.ts +35 -0
- package/dist/symbolic/SymbolicDiff.js +339 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +29 -5
- package/dist/Losses.spec.js +0 -54
- package/dist/Optimizers.edge-cases.spec.d.ts +0 -1
- package/dist/Optimizers.edge-cases.spec.js +0 -29
- package/dist/Optimizers.spec.d.ts +0 -1
- package/dist/Optimizers.spec.js +0 -56
- package/dist/Value.edge-cases.spec.d.ts +0 -1
- package/dist/Value.edge-cases.spec.js +0 -54
- package/dist/Value.grad-flow.spec.d.ts +0 -1
- package/dist/Value.grad-flow.spec.js +0 -24
- package/dist/Value.losses-edge-cases.spec.d.ts +0 -1
- package/dist/Value.losses-edge-cases.spec.js +0 -30
- package/dist/Value.memory.spec.d.ts +0 -1
- package/dist/Value.memory.spec.js +0 -23
- package/dist/Value.nn.spec.d.ts +0 -1
- package/dist/Value.nn.spec.js +0 -111
- package/dist/Value.spec.d.ts +0 -1
- package/dist/Value.spec.js +0 -245
- /package/dist/{Losses.spec.d.ts → __tests__/duplicate-inputs.test.d.ts} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Value } from './Value';
|
|
2
|
+
// 32-bit primes for mixing
|
|
3
|
+
const PRIMES_32 = [
|
|
4
|
+
2654435761, // Golden ratio prime
|
|
5
|
+
805459861,
|
|
6
|
+
3266489917,
|
|
7
|
+
4101842887,
|
|
8
|
+
2484345967,
|
|
9
|
+
3369493747,
|
|
10
|
+
1505335857,
|
|
11
|
+
1267890027,
|
|
12
|
+
];
|
|
13
|
+
// Operation hash constants (same as GraphHashCanonicalizer)
|
|
14
|
+
const OP_HASHES = {
|
|
15
|
+
'+': { h1: 0x9e3779b9, h2: 0x85ebca6b },
|
|
16
|
+
'-': { h1: 0xc2b2ae35, h2: 0x27d4eb2f },
|
|
17
|
+
'*': { h1: 0x165667b1, h2: 0xd3a2646c },
|
|
18
|
+
'/': { h1: 0xfd7046c5, h2: 0xb55a4f09 },
|
|
19
|
+
'pow': { h1: 0x5f356495, h2: 0x3c6ef372 },
|
|
20
|
+
'powValue': { h1: 0x5f356495, h2: 0x3c6ef372 },
|
|
21
|
+
'sqrt': { h1: 0x8f1bbcdc, h2: 0x42a5c977 },
|
|
22
|
+
'exp': { h1: 0x1e3779b9, h2: 0x95ebca6b },
|
|
23
|
+
'log': { h1: 0x2e3779b9, h2: 0xa5ebca6b },
|
|
24
|
+
'abs': { h1: 0x3e3779b9, h2: 0xb5ebca6b },
|
|
25
|
+
'square': { h1: 0x4e3779b9, h2: 0xc5ebca6b },
|
|
26
|
+
'sin': { h1: 0x6e3779b9, h2: 0xe5ebca6b },
|
|
27
|
+
'cos': { h1: 0x7e3779b9, h2: 0xf5ebca6b },
|
|
28
|
+
'tan': { h1: 0x8e3779b9, h2: 0x15ebca6b },
|
|
29
|
+
'asin': { h1: 0x9e3779b9, h2: 0x25ebca6b },
|
|
30
|
+
'acos': { h1: 0xae3779b9, h2: 0x35ebca6b },
|
|
31
|
+
'atan': { h1: 0xbe3779b9, h2: 0x45ebca6b },
|
|
32
|
+
'atan2': { h1: 0xce3779b9, h2: 0x55ebca6b },
|
|
33
|
+
'min': { h1: 0xde3779b9, h2: 0x65ebca6b },
|
|
34
|
+
'max': { h1: 0xee3779b9, h2: 0x75ebca6b },
|
|
35
|
+
'sigmoid': { h1: 0xfe3779b9, h2: 0x85ebca6b },
|
|
36
|
+
'tanh': { h1: 0x1f3779b9, h2: 0x95ebca6b },
|
|
37
|
+
'relu': { h1: 0x2f3779b9, h2: 0xa5ebca6b },
|
|
38
|
+
'softplus': { h1: 0x3f3779b9, h2: 0xb5ebca6b },
|
|
39
|
+
'sum': { h1: 0x9e3779b9, h2: 0x85ebca6b },
|
|
40
|
+
'eigenvalue_custom': { h1: 0x4f3779b9, h2: 0xc5ebca6b },
|
|
41
|
+
};
|
|
42
|
+
function getOpHash(op) {
|
|
43
|
+
if (OP_HASHES[op]) {
|
|
44
|
+
return OP_HASHES[op];
|
|
45
|
+
}
|
|
46
|
+
// Fallback for unknown ops
|
|
47
|
+
let h1 = 2166136261;
|
|
48
|
+
let h2 = 5381;
|
|
49
|
+
for (let i = 0; i < op.length; i++) {
|
|
50
|
+
const c = op.charCodeAt(i);
|
|
51
|
+
h1 = (h1 ^ c) >>> 0;
|
|
52
|
+
h1 = Math.imul(h1, 16777619);
|
|
53
|
+
h2 = ((h2 << 5) + h2 + c) >>> 0;
|
|
54
|
+
}
|
|
55
|
+
return { h1: h1 >>> 0, h2: h2 >>> 0 };
|
|
56
|
+
}
|
|
57
|
+
function combineHashes(h1, h2, position = 0) {
|
|
58
|
+
const p1 = PRIMES_32[position % PRIMES_32.length];
|
|
59
|
+
const p2 = PRIMES_32[(position + 1) % PRIMES_32.length];
|
|
60
|
+
return {
|
|
61
|
+
h1: (h1.h1 ^ Math.imul(h2.h1, p1)) >>> 0,
|
|
62
|
+
h2: (h1.h2 ^ Math.imul(h2.h2, p2)) >>> 0
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function hashLeaf(leafIndex, hasGrad) {
|
|
66
|
+
const gradMask = hasGrad ? 0x12345678 : 0;
|
|
67
|
+
return {
|
|
68
|
+
h1: (leafIndex * PRIMES_32[0] ^ gradMask) >>> 0,
|
|
69
|
+
h2: (leafIndex * PRIMES_32[1] ^ (gradMask << 8)) >>> 0
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Incremental graph builder that tracks structure during construction.
|
|
74
|
+
*
|
|
75
|
+
* Usage:
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const builder = new GraphBuilder(knownParams);
|
|
78
|
+
* const result = builder.build(() => {
|
|
79
|
+
* const a = V.add(params[0], params[1]);
|
|
80
|
+
* const b = V.mul(a, V.C(2));
|
|
81
|
+
* return b;
|
|
82
|
+
* });
|
|
83
|
+
* // result.signature.hash - for kernel lookup
|
|
84
|
+
* // result.signature.leaves - for index mapping at execution time
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export class GraphBuilder {
|
|
88
|
+
leaves = [];
|
|
89
|
+
leafIndexMap = new Map();
|
|
90
|
+
knownParams;
|
|
91
|
+
operations = [];
|
|
92
|
+
/**
|
|
93
|
+
* Create a new graph builder.
|
|
94
|
+
* @param knownParams - Parameters that are known upfront (will be indexed first)
|
|
95
|
+
*/
|
|
96
|
+
constructor(knownParams = []) {
|
|
97
|
+
this.knownParams = new Set(knownParams);
|
|
98
|
+
// Pre-register known parameters
|
|
99
|
+
for (const param of knownParams) {
|
|
100
|
+
this.registerLeaf(param);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Register a leaf Value and return its index.
|
|
105
|
+
* If already registered, returns existing index.
|
|
106
|
+
* New parameters are added dynamically.
|
|
107
|
+
*/
|
|
108
|
+
registerLeaf(value) {
|
|
109
|
+
let index = this.leafIndexMap.get(value);
|
|
110
|
+
if (index !== undefined) {
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
// New leaf - add to end
|
|
114
|
+
index = this.leaves.length;
|
|
115
|
+
this.leaves.push(value);
|
|
116
|
+
this.leafIndexMap.set(value, index);
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Record an operation during graph building.
|
|
121
|
+
* Called automatically by Value.make() when in tracked context.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
recordOp(value) {
|
|
125
|
+
const prev = value.prev;
|
|
126
|
+
// If this is a leaf, register it
|
|
127
|
+
if (prev.length === 0) {
|
|
128
|
+
this.registerLeaf(value);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// For intermediate nodes, register all leaf children
|
|
132
|
+
for (const child of prev) {
|
|
133
|
+
if (child.prev.length === 0) {
|
|
134
|
+
this.registerLeaf(child);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Record this operation for later hash computation
|
|
138
|
+
this.operations.push(value);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build a graph in tracked context and return output + signature.
|
|
142
|
+
*
|
|
143
|
+
* @param fn - Function that builds and returns the output Value
|
|
144
|
+
* @returns Output value and graph signature
|
|
145
|
+
*/
|
|
146
|
+
build(fn) {
|
|
147
|
+
const prevBuilder = Value.currentBuilder;
|
|
148
|
+
Value.currentBuilder = this;
|
|
149
|
+
try {
|
|
150
|
+
const output = fn();
|
|
151
|
+
// Finalize signature
|
|
152
|
+
// Stable sort: params (with grad) first, then params (no grad), then constants
|
|
153
|
+
const sortedLeaves = [...this.leaves].sort((a, b) => {
|
|
154
|
+
const aIsParam = this.knownParams.has(a);
|
|
155
|
+
const bIsParam = this.knownParams.has(b);
|
|
156
|
+
if (aIsParam !== bIsParam)
|
|
157
|
+
return aIsParam ? -1 : 1;
|
|
158
|
+
if (a.requiresGrad !== b.requiresGrad)
|
|
159
|
+
return b.requiresGrad ? 1 : -1;
|
|
160
|
+
return a._id - b._id;
|
|
161
|
+
});
|
|
162
|
+
// Rebuild index map with sorted order
|
|
163
|
+
const finalLeafIndexMap = new Map();
|
|
164
|
+
for (let i = 0; i < sortedLeaves.length; i++) {
|
|
165
|
+
finalLeafIndexMap.set(sortedLeaves[i], i);
|
|
166
|
+
}
|
|
167
|
+
// Compute hash based on graph structure (NOW that we know all leaf assignments)
|
|
168
|
+
// Create stable node ID map: leaves get their indices, operations get sequential IDs
|
|
169
|
+
const nodeIdMap = new Map();
|
|
170
|
+
for (let i = 0; i < sortedLeaves.length; i++) {
|
|
171
|
+
nodeIdMap.set(sortedLeaves[i], i);
|
|
172
|
+
}
|
|
173
|
+
let nextOpId = sortedLeaves.length;
|
|
174
|
+
let hash = { h1: 2166136261, h2: 5381 };
|
|
175
|
+
// Hash leaf configuration first
|
|
176
|
+
for (let i = 0; i < sortedLeaves.length; i++) {
|
|
177
|
+
const leafHash = hashLeaf(i, sortedLeaves[i].requiresGrad);
|
|
178
|
+
hash = combineHashes(hash, leafHash, i);
|
|
179
|
+
}
|
|
180
|
+
// Hash operations in order they were created
|
|
181
|
+
for (const op of this.operations) {
|
|
182
|
+
const prev = op.prev;
|
|
183
|
+
// Assign ID to this operation
|
|
184
|
+
if (!nodeIdMap.has(op)) {
|
|
185
|
+
nodeIdMap.set(op, nextOpId++);
|
|
186
|
+
}
|
|
187
|
+
let opName = op._op || 'unknown';
|
|
188
|
+
// Normalize operations
|
|
189
|
+
if (opName === 'sum')
|
|
190
|
+
opName = '+';
|
|
191
|
+
if (opName === 'powValue' && prev.length === 2) {
|
|
192
|
+
const exponent = prev[1];
|
|
193
|
+
if (exponent.prev.length === 0 && exponent.data === 2 && !exponent.requiresGrad) {
|
|
194
|
+
opName = 'square';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const opHash = getOpHash(opName);
|
|
198
|
+
hash = combineHashes(hash, opHash, 0);
|
|
199
|
+
// Hash children by their stable IDs
|
|
200
|
+
for (let i = 0; i < prev.length; i++) {
|
|
201
|
+
const child = prev[i];
|
|
202
|
+
if (!nodeIdMap.has(child)) {
|
|
203
|
+
nodeIdMap.set(child, nextOpId++);
|
|
204
|
+
}
|
|
205
|
+
const childId = nodeIdMap.get(child);
|
|
206
|
+
const childHash = {
|
|
207
|
+
h1: childId * PRIMES_32[2],
|
|
208
|
+
h2: childId * PRIMES_32[3]
|
|
209
|
+
};
|
|
210
|
+
hash = combineHashes(hash, childHash, i + 1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const hashString = hash.h1.toString(16).padStart(8, '0') +
|
|
214
|
+
hash.h2.toString(16).padStart(8, '0');
|
|
215
|
+
return {
|
|
216
|
+
output,
|
|
217
|
+
signature: {
|
|
218
|
+
hash: hashString,
|
|
219
|
+
leaves: sortedLeaves,
|
|
220
|
+
leafIndexMap: finalLeafIndexMap
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
Value.currentBuilder = prevBuilder;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get current signature without finalizing (for debugging)
|
|
230
|
+
*/
|
|
231
|
+
getCurrentSignature() {
|
|
232
|
+
return {
|
|
233
|
+
hash: 'incomplete',
|
|
234
|
+
leaves: [...this.leaves]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Value } from './Value';
|
|
2
|
+
export interface CanonicalResult {
|
|
3
|
+
/** Canonical string uniquely identifying graph structure */
|
|
4
|
+
canon: string;
|
|
5
|
+
/** Maps Value nodes to their parameter indices */
|
|
6
|
+
paramMap: Map<Value, number>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Canonicalize a computation graph WITHOUT sorting (fast, fewer matches).
|
|
10
|
+
*
|
|
11
|
+
* This is faster than the sorted version but produces different signatures
|
|
12
|
+
* for reordered commutative operations.
|
|
13
|
+
*
|
|
14
|
+
* @param output - The output Value of the computation graph
|
|
15
|
+
* @param params - Array of parameter Values for this residual/objective
|
|
16
|
+
* @returns Canonical string and parameter mapping
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalizeGraphNoSort(output: Value, params: Value[]): CanonicalResult;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CANONICAL STRING REPRESENTATION (NO SORTING)
|
|
3
|
+
*
|
|
4
|
+
* Fast variant that doesn't sort children of commutative operations.
|
|
5
|
+
* This means fewer kernel matches (add(a,b) != add(b,a)), but much faster.
|
|
6
|
+
*
|
|
7
|
+
* Uses traverse counter optimization: instead of Map<Value, number> lookups,
|
|
8
|
+
* stamps each Value with a traverse ID for O(1) property access.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
// Global traverse counter for memoization
|
|
13
|
+
let globalTraverseId = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Canonicalize a computation graph WITHOUT sorting (fast, fewer matches).
|
|
16
|
+
*
|
|
17
|
+
* This is faster than the sorted version but produces different signatures
|
|
18
|
+
* for reordered commutative operations.
|
|
19
|
+
*
|
|
20
|
+
* @param output - The output Value of the computation graph
|
|
21
|
+
* @param params - Array of parameter Values for this residual/objective
|
|
22
|
+
* @returns Canonical string and parameter mapping
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export function canonicalizeGraphNoSort(output, params) {
|
|
27
|
+
const t0 = performance.now();
|
|
28
|
+
const leafToId = new Map();
|
|
29
|
+
const allLeaves = new Set();
|
|
30
|
+
// Increment global traverse ID for this canonicalization
|
|
31
|
+
const traverseId = ++globalTraverseId;
|
|
32
|
+
// Phase 1: Discover all leaves in the graph (iterative)
|
|
33
|
+
const discoverStack = [output];
|
|
34
|
+
const discoverVisited = new Set();
|
|
35
|
+
while (discoverStack.length > 0) {
|
|
36
|
+
const node = discoverStack.pop();
|
|
37
|
+
if (discoverVisited.has(node))
|
|
38
|
+
continue;
|
|
39
|
+
discoverVisited.add(node);
|
|
40
|
+
const prev = node.prev;
|
|
41
|
+
if (prev.length === 0) {
|
|
42
|
+
allLeaves.add(node);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
for (const child of prev) {
|
|
46
|
+
discoverStack.push(child);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const t1 = performance.now();
|
|
51
|
+
// Phase 2: Assign IDs in stable, deterministic order
|
|
52
|
+
let nextId = 0;
|
|
53
|
+
const paramSet = new Set(params);
|
|
54
|
+
for (const param of params) {
|
|
55
|
+
if (allLeaves.has(param)) {
|
|
56
|
+
leafToId.set(param, nextId++);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const remainingLeaves = Array.from(allLeaves)
|
|
60
|
+
.filter(leaf => !paramSet.has(leaf));
|
|
61
|
+
for (const leaf of remainingLeaves) {
|
|
62
|
+
leafToId.set(leaf, nextId++);
|
|
63
|
+
}
|
|
64
|
+
const t2 = performance.now();
|
|
65
|
+
// Phase 3: Build canonical expression (NO SORTING, NO RECURSION!)
|
|
66
|
+
let nextNodeId = nextId; // Start after leaf IDs
|
|
67
|
+
// Build a compact representation: array of [nodeId, op, childIds...]
|
|
68
|
+
const nodeExprs = [];
|
|
69
|
+
// Topological sort to process nodes bottom-up (iterative)
|
|
70
|
+
const topoOrder = [];
|
|
71
|
+
const topoVisited = new Set();
|
|
72
|
+
const topoStack = [output];
|
|
73
|
+
while (topoStack.length > 0) {
|
|
74
|
+
const node = topoStack[topoStack.length - 1];
|
|
75
|
+
if (topoVisited.has(node)) {
|
|
76
|
+
topoStack.pop();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const prev = node.prev;
|
|
80
|
+
// If leaf, mark visited and add to order
|
|
81
|
+
if (prev.length === 0 || leafToId.has(node)) {
|
|
82
|
+
topoVisited.add(node);
|
|
83
|
+
topoOrder.push(node);
|
|
84
|
+
topoStack.pop();
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Check if all children processed
|
|
88
|
+
let allChildrenProcessed = true;
|
|
89
|
+
for (let i = prev.length - 1; i >= 0; i--) {
|
|
90
|
+
if (!topoVisited.has(prev[i])) {
|
|
91
|
+
allChildrenProcessed = false;
|
|
92
|
+
topoStack.push(prev[i]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (allChildrenProcessed) {
|
|
96
|
+
topoVisited.add(node);
|
|
97
|
+
topoOrder.push(node);
|
|
98
|
+
topoStack.pop();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const tTraversal = performance.now();
|
|
102
|
+
// Process nodes in topological order (leaves first)
|
|
103
|
+
// Use traverse counter stamping instead of Map lookups
|
|
104
|
+
for (const node of topoOrder) {
|
|
105
|
+
const nodeAny = node;
|
|
106
|
+
// Skip if already processed in this traverse
|
|
107
|
+
if (nodeAny._canonTraverseId === traverseId)
|
|
108
|
+
continue;
|
|
109
|
+
// Leaf node
|
|
110
|
+
const leafId = leafToId.get(node);
|
|
111
|
+
if (leafId !== undefined) {
|
|
112
|
+
nodeAny._canonTraverseId = traverseId;
|
|
113
|
+
nodeAny._canonNodeId = leafId;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const prev = node.prev;
|
|
117
|
+
if (prev.length === 0)
|
|
118
|
+
continue; // Skip empty prev
|
|
119
|
+
// Assign new ID and stamp it
|
|
120
|
+
const myId = nextNodeId++;
|
|
121
|
+
nodeAny._canonTraverseId = traverseId;
|
|
122
|
+
nodeAny._canonNodeId = myId;
|
|
123
|
+
let op = node._op || 'unknown';
|
|
124
|
+
// Normalize pow(x, const_2) -> square(x)
|
|
125
|
+
if (op === 'powValue' && prev.length === 2) {
|
|
126
|
+
const exponent = prev[1];
|
|
127
|
+
const exponentId = leafToId.get(exponent);
|
|
128
|
+
if (exponentId !== undefined && exponent.data === 2 && !exponent.requiresGrad) {
|
|
129
|
+
op = 'square';
|
|
130
|
+
const childId = prev[0]._canonNodeId;
|
|
131
|
+
nodeExprs.push([myId, op, [childId]]);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (op === 'sum')
|
|
136
|
+
op = '+';
|
|
137
|
+
// All operations: just use children as-is (no flattening)
|
|
138
|
+
const childIds = prev.map(p => p._canonNodeId);
|
|
139
|
+
nodeExprs.push([myId, op, childIds]);
|
|
140
|
+
}
|
|
141
|
+
const outputId = output._canonNodeId;
|
|
142
|
+
const tTraversalEnd = performance.now();
|
|
143
|
+
// Build compact canonical signature from node expressions
|
|
144
|
+
const tStringStart = performance.now();
|
|
145
|
+
// Build leaf lookup for gradient flags (id -> 'g' suffix or '')
|
|
146
|
+
const leafGradFlags = new Map();
|
|
147
|
+
for (const [leaf, id] of leafToId) {
|
|
148
|
+
leafGradFlags.set(id, leaf.requiresGrad ? 'g' : '');
|
|
149
|
+
}
|
|
150
|
+
// Build expression parts (compact format)
|
|
151
|
+
const exprParts = [];
|
|
152
|
+
for (const [nodeId, op, childIds] of nodeExprs) {
|
|
153
|
+
const childRefs = childIds.map(id => {
|
|
154
|
+
const gradFlag = leafGradFlags.get(id);
|
|
155
|
+
if (gradFlag !== undefined) {
|
|
156
|
+
return `${id}${gradFlag}`;
|
|
157
|
+
}
|
|
158
|
+
return `n${id}`;
|
|
159
|
+
}).join(',');
|
|
160
|
+
exprParts.push(`n${nodeId}:(${op},${childRefs})`);
|
|
161
|
+
}
|
|
162
|
+
const expr = exprParts.join(';');
|
|
163
|
+
// Phase 4: Build param list
|
|
164
|
+
const paramList = [];
|
|
165
|
+
for (let i = 0; i < params.length; i++) {
|
|
166
|
+
const param = params[i];
|
|
167
|
+
if (!allLeaves.has(param))
|
|
168
|
+
continue;
|
|
169
|
+
const gradFlag = param.requiresGrad ? 'g' : '';
|
|
170
|
+
paramList.push(`${i}${gradFlag}`);
|
|
171
|
+
}
|
|
172
|
+
// For single-node leaf graphs, include the leaf ID
|
|
173
|
+
const finalExpr = expr.length > 0 ? expr : `${outputId}${leafGradFlags.get(outputId) || ''}`;
|
|
174
|
+
const canon = `${paramList.join(',')}|${finalExpr}`;
|
|
175
|
+
const tStringEnd = performance.now();
|
|
176
|
+
const t3 = performance.now();
|
|
177
|
+
// Build reverse map
|
|
178
|
+
const paramMap = new Map();
|
|
179
|
+
for (const [leaf, id] of leafToId) {
|
|
180
|
+
paramMap.set(leaf, id);
|
|
181
|
+
}
|
|
182
|
+
const t4 = performance.now();
|
|
183
|
+
const total = t4 - t0;
|
|
184
|
+
if (total > 10) {
|
|
185
|
+
const traversalTime = tTraversalEnd - tTraversal;
|
|
186
|
+
const stringBuildTime = tStringEnd - tStringStart;
|
|
187
|
+
console.log(`[GraphCanonicalizerNoSort] Total: ${total.toFixed(0)}ms | Phase1(discover): ${(t1 - t0).toFixed(0)}ms | Phase2(assign): ${(t2 - t1).toFixed(0)}ms | Phase3(expr): ${(t3 - t2).toFixed(0)}ms [traversal: ${traversalTime.toFixed(0)}ms, stringBuild: ${stringBuildTime.toFixed(0)}ms] | Phase4(build): ${(t4 - t3).toFixed(0)}ms | Nodes: ${discoverVisited.size} | NodeExprs: ${nodeExprs.length} | keyLength: ${canon.length}`);
|
|
188
|
+
}
|
|
189
|
+
return { canon, paramMap };
|
|
190
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Value } from './Value';
|
|
2
|
+
/**
|
|
3
|
+
* HASH-BASED CANONICAL REPRESENTATION
|
|
4
|
+
*
|
|
5
|
+
* Fast canonicalization using dual 32-bit integer hashing.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Each node gets a hash based on its operation and children
|
|
9
|
+
* - Position-dependent hashing (preserves order and multiplicity)
|
|
10
|
+
* - Direct integer operations (NO STRING HASHING!)
|
|
11
|
+
* - Parameters maintain topological order in signature
|
|
12
|
+
*
|
|
13
|
+
* Format: "0g,1g,2g,...|<hex_hash>"
|
|
14
|
+
*
|
|
15
|
+
* Benefits:
|
|
16
|
+
* - O(n) complexity (no sorting!)
|
|
17
|
+
* - Dual 32-bit hash (64-bit space, no BigInt overhead)
|
|
18
|
+
* - Preserves multiplicity: add(a,a,b) != add(a,b)
|
|
19
|
+
* - Recursion-free (stack-based iteration)
|
|
20
|
+
* - Pure integer operations (no string conversion!)
|
|
21
|
+
*
|
|
22
|
+
* Note: add(a,b) != add(b,a) - not order-independent
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export interface HashCanonicalResult {
|
|
27
|
+
/** Canonical string with parameter list and expression hash */
|
|
28
|
+
canon: string;
|
|
29
|
+
/** Maps Value nodes to their parameter indices */
|
|
30
|
+
paramMap: Map<Value, number>;
|
|
31
|
+
/** Debug: full string expression (if enabled) */
|
|
32
|
+
debugExpr?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Canonicalize a computation graph using hash-based signatures.
|
|
36
|
+
*
|
|
37
|
+
* Much faster than string-based canonicalization - O(n) with no sorting.
|
|
38
|
+
*
|
|
39
|
+
* @param output - The output Value of the computation graph
|
|
40
|
+
* @param params - Array of parameter Values for this residual/objective
|
|
41
|
+
* @param debug - If true, also generate string expression for debugging
|
|
42
|
+
* @returns Canonical string with hash and parameter mapping
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function canonicalizeGraphHash(output: Value, params: Value[], debug?: boolean): HashCanonicalResult;
|