asc-community / asc-community/GenericTensor

Determinant and Inverse return wrong values when called from more than one thread

Open
#40 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
52
Forks
6
PR merge metrics
No merged PRs in 30d

Description

Two process-wide statics are read and written with no synchronisation, so matrix operations called from more than one thread return **wrong numbers**. Nothing throws, nothing is malformed, and no result looks suspicious.

I found this from the other side: [AngouriMath](https://github.com/asc-community/AngouriMath) uses `GenTensor` for its symbolic matrices, and a determinant computed on a background thread was coming back numerically wrong. AngouriMath's issue is [#1219](https://github.com/asc-community/AngouriMath/issues/1219).

### 1. The scratch-matrix pool

`SquareMatrixFactory.GetMatrix` hands **every caller the same `GenTensor`** for a given size, and the caller then writes into it — `Inversion.GetCofactorMatrix(t, temp, ...)` fills the matrix it is given. Two threads taking a determinant or an inverse of the same size get the same buffer and overwrite each other's minors between the write and the read.

Sixty 5×5 `int` matrices, each computed once sequentially for truth and then rebuilt and recomputed under `Parallel.For`:

```
DeterminantLaplace 53 of 60 disagree
Adjoint 60 of 60 disagree
```

Around 10 ms, every run. This is not a rare interleaving, it is the normal case.

`DeterminantGaussianSafeDivision` is unaffected — it works on its own copy and never asks the pool for anything.

The lock inside `GetMatrix` does not help and is itself incomplete: the list is indexed *after* the lock is released, so a concurrent `Add` can reallocate the backing array underneath the reader.

### 2. The compiled-operation cache

`ExpressionCompiler.storage` is a plain `Dictionary`, written by whichever thread first asks for a given `(operation, rank, parallel)`. `Dictionary<,>` is documented as safe for concurrent readers only while nobody is writing.

Reached cold from several threads it fails with the runtime's own diagnostic:

```
System.InvalidOperationException: Operations that change non-concurrent collections must
have exclusive access. A concurrent update was performed on this collection and corrupted
its state. The collection's state is no longer correct.
...
The given key '(Subtraction, 1, False)' was not present in the dictionary.
```

Worth noting that this one hides easily. My first test computed its expected values sequentially and only then went parallel, which populated every key before the threads started — the cache holds a handful of entries, so a warm-up makes the defect invisible and the test passes against the broken code.

### Fix

I have opened a PR. Both are small: `[ThreadStatic]` for the pool, since a scratch buffer has nothing to share between threads, and `ConcurrentDictionary` for the cache. The Laplace determinant comes out about 5% *faster*, because the pool is now taken once at the top of the recursion instead of re-checked and re-indexed at every one of its n! nodes.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with SquareMatrixFactory.GetMatrix and ExpressionCompiler.storage, then reproduce the reported determinant/inverse failures with Parallel.For, including a cold cache. Done means concurrent matrix operations return the sequentially verified values without collection exceptions or shared-buffer corruption.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.