Compare commits

..
7 Commits
Author SHA1 Message Date
Youmu Liu 2e7c3040f4 rewrite of swiss, swiss will not be public but l4 specific 2026-10-08 18:15:05 +08:00
Youmu Liu 1845a00390 few edit 2026-09-28 18:11:49 +08:00
Youmu Liu 194e869724 few adit 2026-09-23 18:19:33 +08:00
Youmu Liu 7ddfc36ac5 few adit 2026-09-21 20:39:43 +08:00
Youmu Liu 2c17f6c108 start nfa afterall 2026-09-20 18:29:44 +08:00
Youmu Liu 044b153463 add doc 2026-09-18 18:28:09 +08:00
Youmu Liu 6e6bd7794b add doc 2026-09-17 18:25:10 +08:00
25 changed files with 1252 additions and 391 deletions
+9 -9
View File
@@ -231,7 +231,7 @@ namespace Nitload.Benchmark
{ {
nuint key = _residentKeys[i]; nuint key = _residentKeys[i];
nuint val = key ^ unchecked((nuint)0xDEAD_BEEF); nuint val = key ^ unchecked((nuint)0xDEAD_BEEF);
_freshSwiss.TryAddOrSet(key, val, out _); SwissTable.Writer.TrySet(_freshSwiss, key, val);
_freshDict[key] = val; _freshDict[key] = val;
} }
} }
@@ -273,7 +273,7 @@ namespace Nitload.Benchmark
{ {
nuint key = _residentKeys[i]; nuint key = _residentKeys[i];
nuint val = key ^ unchecked((nuint)0xDEAD_BEEF); // simulated cache pointer nuint val = key ^ unchecked((nuint)0xDEAD_BEEF); // simulated cache pointer
_swissLookup.TryAddOrSet(key, val, out _); SwissTable.Writer.TrySet(_swissLookup, key, val);
_dictLookup[key] = val; _dictLookup[key] = val;
} }
} }
@@ -284,7 +284,7 @@ namespace Nitload.Benchmark
SwissTable swiss = _freshSwiss; SwissTable swiss = _freshSwiss;
nuint[] keys = _residentKeys; nuint[] keys = _residentKeys;
for (int i = 0; i < keys.Length; i++) { for (int i = 0; i < keys.Length; i++) {
swiss.TryAddOrSet(keys[i], keys[i] ^ unchecked((nuint)0xDEAD_BEEF), out _); SwissTable.Writer.TrySet(swiss, keys[i], keys[i] ^ unchecked((nuint)0xDEAD_BEEF));
} }
} }
@@ -296,7 +296,7 @@ namespace Nitload.Benchmark
SwissTable swiss = _swissLookup; SwissTable swiss = _swissLookup;
for (int i = 0; i < keys.Length; i++) for (int i = 0; i < keys.Length; i++)
{ {
if (swiss.TryGet(keys[i], out nuint value)) { if (SwissTable.Reader.TryGet(swiss, keys[i], out nuint value)) {
sum += value; sum += value;
} }
} }
@@ -311,7 +311,7 @@ namespace Nitload.Benchmark
SwissTable swiss = _swissLookup; SwissTable swiss = _swissLookup;
for (int i = 0; i < keys.Length; i++) for (int i = 0; i < keys.Length; i++)
{ {
if (swiss.TryGet(keys[i], out nuint value)) { if (SwissTable.Reader.TryGet(swiss, keys[i], out nuint value)) {
sum += value; sum += value;
} }
} }
@@ -336,7 +336,7 @@ namespace Nitload.Benchmark
if (op >= 0) if (op >= 0)
{ {
// Cache hit: lookup existing entry // Cache hit: lookup existing entry
if (swiss.TryGet(resident[op], out nuint value)) if (SwissTable.Reader.TryGet(swiss, resident[op], out nuint value))
{ {
sum += value; sum += value;
} }
@@ -345,15 +345,15 @@ namespace Nitload.Benchmark
// Cache miss: evict a cold entry, insert a new one, then lookup // Cache miss: evict a cold entry, insert a new one, then lookup
int evictIdx = evict[evictCursor % evict.Length]; int evictIdx = evict[evictCursor % evict.Length];
evictCursor++; evictCursor++;
swiss.TryRemove(resident[evictIdx], out _); SwissTable.Writer.TryRemove(swiss, resident[evictIdx], out _);
int extIdx = externalCursor % external.Length; int extIdx = externalCursor % external.Length;
externalCursor++; externalCursor++;
nuint newKey = external[extIdx]; nuint newKey = external[extIdx];
nuint newVal = newKey ^ unchecked((nuint)0xDEAD_BEEF); nuint newVal = newKey ^ unchecked((nuint)0xDEAD_BEEF);
swiss.TryAddOrSet(newKey, newVal, out _); SwissTable.Writer.TryAdd(swiss, newKey, newVal);
if (swiss.TryGet(newKey, out nuint v)) { if (SwissTable.Reader.TryGet(swiss, newKey, out nuint v)) {
sum += v; sum += v;
} }
} }
@@ -1,130 +0,0 @@
// -----------------------------------------------------------------------------
// file="SwissTable.X86"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Numerics;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace Nitload.Common.Collections
{
public partial class SwissTable
{
private unsafe class BucketScannerX86 : IBucketScanner
{
public bool TryFind(
nuint key,
uint hash,
Bucket* bucket,
out nuint value
)
{
byte ctrl = (byte)((hash >> EntryMaskWidth) & 0x7F);
Vector128<byte> ctrlVector = Vector128.Create(ctrl);
Vector128<byte> bucketCtrlVector = Sse2.LoadVector128((byte*)&bucket->Control);
Vector128<byte> cmpResult = Sse2.CompareEqual(ctrlVector, bucketCtrlVector);
int mask = Sse2.MoveMask(cmpResult);
while (mask != 0)
{
int bitPos = BitOperations.TrailingZeroCount(mask);
int index = bitPos;
if (bucket->Keys[index] == key)
{
value = bucket->Values[index];
return true;
}
mask &= mask - 1;
}
value = default;
return false;
}
public bool TryRemove(
nuint key,
uint hash,
Bucket* bucket,
out nuint value
)
{
byte ctrl = (byte)((hash >> EntryMaskWidth) & 0x7F);
Vector128<byte> ctrlVector = Vector128.Create(ctrl);
Vector128<byte> bucketCtrl = Sse2.LoadVector128((byte*)&bucket->Control);
Vector128<byte> matchMask = Sse2.CompareEqual(ctrlVector, bucketCtrl);
int matches = Sse2.MoveMask(matchMask);
while (matches != 0)
{
int bitPos = BitOperations.TrailingZeroCount(matches);
int index = bitPos;
if (bucket->Keys[index] == key)
{
value = bucket->Values[index];
bucket->Control[index] = 0xFE; // mark as DELETED
// no need to remove Key/Value
return true;
}
matches &= matches - 1;
}
value = default;
return false;
}
public bool TrySet(
nuint key,
uint hash,
Bucket* bucket,
nuint value,
out bool isNewKey
)
{
byte ctrl = (byte)((hash >> EntryMaskWidth) & 0x7F); // H2: upper 8 bits (highest bit is naturally 0)
Vector128<byte> ctrlVector = Vector128.Create(ctrl);
Vector128<byte> bucketCtrl = Sse2.LoadVector128(
(byte*)&bucket->Control);
// === Phase 1: Look up an existing key ===
Vector128<byte> matchMask = Sse2.CompareEqual(ctrlVector, bucketCtrl);
int matches = Sse2.MoveMask(matchMask);
while (matches != 0)
{
int bitPos = BitOperations.TrailingZeroCount(matches);
int index = bitPos;
if (bucket->Keys[index] == key)
{
// Key already exists -> update value
bucket->Values[index] = value;
isNewKey = false;
return true;
}
matches &= matches - 1;
}
// === Phase 2: Track an available insertion slot ===
int emptyOrDeleted = Sse2.MoveMask(bucketCtrl);
if (emptyOrDeleted == 0)
{
isNewKey = false;
return false; // bucket full
}
int slot = BitOperations.TrailingZeroCount(emptyOrDeleted);
bucket->Control[slot] = ctrl;
bucket->Keys[slot] = key;
bucket->Values[slot] = value;
isNewKey = true;
return true;
}
}
}
}
+49 -148
View File
@@ -5,143 +5,41 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using Nitload.Common.HighPerformance.ProcessorAbstraction;
using System; using System;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace Nitload.Common.Collections namespace Nitload.Common.Collections
{ {
/// <summary> public sealed unsafe partial class SwissTable
///
/// </summary>
public sealed unsafe partial class SwissTable : IDisposable
{ {
private const int BucketCount = 1 << EntryMaskWidth;
private const int BucketSize = 16; private const int BucketSize = 16;
private const uint CrcMagic = 0xffff9961; // Magic number for CRC32C, used in hash computation private const uint CrcMagic = 0xffff9961; // Magic number for CRC32C, used in hash computation
private const int EntryMaskWidth = 10; // 1024 entries total, 16 per bucket private const int EntryMaskWidth = 10; // 1024 entries total, 16 per bucket
public const int MaxCapacity = BucketCount * BucketSize; // 256 entries per bucket, 1024 buckets
public static readonly nuint RequestedNativeMemorySize = (nuint)Unsafe.SizeOf<Entry>(); private const int GroupCount = 1 << EntryMaskWidth;
public const int MaxCapacity = GroupCount * BucketSize; // 256 entries per bucket, 1024 buckets
private readonly Entry* _entries; public static readonly nuint RequestedNativeMemorySize = (nuint)Unsafe.SizeOf<byte>() + (nuint)Unsafe.SizeOf<M128>();
private readonly Action<nuint> _freeHandle;
private bool _disposed; private byte* _controlBytes;
private SwissTable( private SwissTable(
Entry* buffer, byte* buffer
Action<nuint> freeHandle
) )
{ {
_entries = buffer; _controlBytes = buffer;
_freeHandle = freeHandle;
} }
public static SwissTable Create() private M128* KvpArray => (M128*)(_controlBytes + MaxCapacity);
{
Entry* buffer = (Entry*)NativeMemory.AlignedAlloc((nuint)Unsafe.SizeOf<Entry>(), 16);
for (int i = 0; i < BucketCount; i++)
{
// Set all control bytes to 0xFF (empty)
Unsafe.InitBlock(&(*buffer)[i].Control, 0xFF, BucketSize);
}
return new SwissTable(buffer, handle => NativeMemory.AlignedFree((void*)handle));
}
public static SwissTable Create( private static void Initialize(
Func<nuint, nuint> allocator, byte* buffer
Action<nuint> freeHandle
) )
{ {
Entry* buffer = (Entry*)allocator(RequestedNativeMemorySize); Unsafe.InitBlock(buffer, 0b10000000, BucketSize);
for (int i = 0; i < BucketCount; i++)
{
// Set all control bytes to 0xFF (empty)
Unsafe.InitBlock(&(*buffer)[i].Control, 0xFF, BucketSize);
}
return new SwissTable(buffer, freeHandle);
}
public void Dispose()
{
if (_disposed) {
return;
}
_disposed = true;
_freeHandle((nuint)_entries);
GC.SuppressFinalize(this);
}
public bool TryAddOrSet(
nuint key,
nuint value,
out bool isNewKey
)
{
uint hash = BitOperations.Crc32C(CrcMagic, (ulong)key);
ushort entry = (ushort)(hash & (BucketCount - 1));
Bucket* bucket = &(*_entries)[entry];
return IBucketScanner.Current.TrySet(key, hash, bucket, value, out isNewKey);
}
public bool TryGet(
nuint key,
out nuint value
)
{
uint hash = BitOperations.Crc32C(CrcMagic, key);
ushort entry = (ushort)(hash & (BucketCount - 1));
Bucket* bucket = &(*_entries)[entry];
return IBucketScanner.Current.TryFind(key, hash, bucket, out value);
/*
* hash = CRC32 of key
* entry = low 8 of hash
* ctrl = 8-15 of hash
* index = rest of hash
* find value of hash
*/
}
public bool TryRemove(
nuint key,
out nuint value
)
{
uint hash = BitOperations.Crc32C(CrcMagic, key);
ushort entry = (ushort)(hash & (BucketCount - 1));
Bucket* bucket = &(*_entries)[entry];
return IBucketScanner.Current.TryRemove(key, hash, bucket, out value);
}
internal static SwissTable Create(
nuint bufferPtr,
Action<nuint> FreeHandle
)
{
Entry* buffer = (Entry*)bufferPtr;
for (int i = 0; i < BucketCount; i++)
{
// Set all control bytes to 0xFF (empty)
Unsafe.InitBlock(&(*buffer)[i].Control, 0xFF, BucketSize);
}
return new SwissTable(buffer, FreeHandle);
}
~SwissTable()
{
Dispose();
}
[InlineArray(BucketCount)]
private struct Entry
{
public Bucket Bucket;
} }
private unsafe struct Bucket private unsafe struct Bucket
@@ -153,45 +51,48 @@ namespace Nitload.Common.Collections
} }
private interface IBucketScanner public static class Reader
{ {
static IBucketScanner Current { get; } = Create(); public static bool TryGet(
SwissTable table,
bool TryFind(
nuint key, nuint key,
uint Hash,
Bucket* bucket,
out nuint value out nuint value
); )
unsafe bool TryRemove(
nuint key,
uint hash,
Bucket* bucket,
out nuint value
);
bool TrySet(
nuint key,
uint hash,
Bucket* bucket,
nuint value,
out bool isNewKey
);
private static IBucketScanner Create()
{ {
if (Sse42.IsSupported) throw new NotImplementedException("SwissTable reader is awaiting a redesigned storage and lookup implementation.");
{
return new BucketScannerX86();
} else if (ArmBase.IsSupported)
{
throw new NotSupportedException("ARM architecture is currently not supported for bucket scanning. Please use an x86 platform.");
} else
{
throw new NotSupportedException("No suitable bucket scanner implementation found for the current platform.");
} }
}
public static class Writer
{
public static bool TryAdd(
SwissTable table,
nuint key,
nuint value
)
{
throw new NotImplementedException("SwissTable writer is awaiting a redesigned storage and insertion implementation.");
}
public static bool TryRemove(
SwissTable table,
nuint key,
out nuint value
)
{
throw new NotImplementedException("SwissTable writer is awaiting a redesigned storage and removal implementation.");
}
public static bool TrySet(
SwissTable table,
nuint key,
nuint value
)
{
throw new NotImplementedException("SwissTable writer is awaiting a redesigned storage and update implementation.");
} }
} }
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using Nitload.Common.Collections;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -26,7 +27,7 @@ namespace Nitload.Common.HighPerformance.Memory
nuint origin nuint origin
) )
{ {
return _map.TryGet(origin, out _); return SwissTable.Reader.TryGet(_map, origin, out _);
} }
public int DebugEvict( public int DebugEvict(
@@ -5,6 +5,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using Nitload.Common.Collections;
namespace Nitload.Common.HighPerformance.Memory namespace Nitload.Common.HighPerformance.Memory
{ {
public partial class L4 public partial class L4
@@ -49,7 +51,7 @@ namespace Nitload.Common.HighPerformance.Memory
if (Interlocked.CompareExchange(ref head->RefCount, int.MinValue, 0) == 0) if (Interlocked.CompareExchange(ref head->RefCount, int.MinValue, 0) == 0)
{ {
// EvictLine(i + index); // EvictLine(i + index);
_ = _map.TryRemove(head->Origin, out _); _ = SwissTable.Writer.TryRemove(_map, head->Origin, out _);
IL4Predict predict = CurrentPredict; IL4Predict predict = CurrentPredict;
predict.WriteBack(head->Origin, head->CustomizeProfile, new ReadOnlySpan<byte>((byte*)(ulong)(head->PositionMask + _baseAddress), (int)(ulong)head->SizeMask)); predict.WriteBack(head->Origin, head->CustomizeProfile, new ReadOnlySpan<byte>((byte*)(ulong)(head->PositionMask + _baseAddress), (int)(ulong)head->SizeMask));
_bufferAllocator.Free(index); _bufferAllocator.Free(index);
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using Nitload.Common.Collections;
using System.ComponentModel; using System.ComponentModel;
using System.Numerics; using System.Numerics;
@@ -32,7 +33,7 @@ namespace Nitload.Common.HighPerformance.Memory
{ {
ref CacheLineDescription handle = ref span[i]; ref CacheLineDescription handle = ref span[i];
nuint address = handle.Address; nuint address = handle.Address;
if (!_map.TryGet(address, out _)) if (!SwissTable.Reader.TryGet(_map, address, out _))
{ {
// miss and record // miss and record
span[missIndex] = handle; span[missIndex] = handle;
@@ -47,7 +48,7 @@ namespace Nitload.Common.HighPerformance.Memory
ref CacheLineDescription pHandle = ref span[i]; ref CacheLineDescription pHandle = ref span[i];
nuint address = pHandle.Address; nuint address = pHandle.Address;
int index; int index;
if (_map.TryGet(address, out nuint handle)) if (SwissTable.Reader.TryGet(_map, address, out nuint handle))
{ {
head = (CacheLineMetadata*)handle; head = (CacheLineMetadata*)handle;
index = (int)(head - _head); index = (int)(head - _head);
@@ -93,7 +94,7 @@ namespace Nitload.Common.HighPerformance.Memory
// pre fetch them here // pre fetch them here
predict.Prefetch(head->Origin, head->CustomizeProfile, new Span<byte>((byte*)(head->PositionMask + _baseAddress), (int)head->SizeMask)); predict.Prefetch(head->Origin, head->CustomizeProfile, new Span<byte>((byte*)(head->PositionMask + _baseAddress), (int)head->SizeMask));
_ = _map.TryAddOrSet(address, (nuint)head, out _); _ = SwissTable.Writer.TrySet(_map, address, (nuint)head);
} }
_ = Interlocked.Add(ref _aheadCount, actualCount); _ = Interlocked.Add(ref _aheadCount, actualCount);
} }
@@ -162,7 +162,7 @@ namespace Nitload.Common.HighPerformance.Memory
} }
while (true) while (true)
{ {
if (_map.TryGet(prediction.GetHashCodeLong(), out nuint entryHandle)) if (SwissTable.Reader.TryGet(_map, prediction.GetHashCodeLong(), out nuint entryHandle))
{ {
// cache hit // cache hit
CacheLineMetadata* entry = (CacheLineMetadata*)entryHandle; CacheLineMetadata* entry = (CacheLineMetadata*)entryHandle;
@@ -16,22 +16,21 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
public readonly struct LogicalCore public readonly struct LogicalCore
{ {
public readonly uint GlobalCpuId; public readonly uint ProcessorGroupId;
public readonly uint NumaGroupId; public readonly uint SocketId;
public readonly uint CpuIndex;
/// <summary>
/// Stays 0 on linux.
/// </summary>
public readonly uint NumaNodeId; public readonly uint NumaNodeId;
internal LogicalCore( internal LogicalCore(
uint globalCpuId, uint processorGroupId,
uint numaGroupId, uint socketId,
uint cpuIndex,
uint numaNodeId uint numaNodeId
) )
{ {
GlobalCpuId = globalCpuId; ProcessorGroupId = processorGroupId;
NumaGroupId = numaGroupId; SocketId = socketId;
CpuIndex = cpuIndex;
NumaNodeId = numaNodeId; NumaNodeId = numaNodeId;
} }
@@ -33,7 +33,7 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
if (_numaNodes.Length == 0) { if (_numaNodes.Length == 0) {
throw new InvalidOperationException("Numa info has not been loaded."); throw new InvalidOperationException("Numa info has not been loaded.");
} }
if ((numaId < 0) || (numaId >= _numaNodes.Length)) { if ((numaId < 0) || !ContainsNode((uint)numaId)) {
throw new ArgumentOutOfRangeException(nameof(numaId), "Invalid NUMA node ID."); throw new ArgumentOutOfRangeException(nameof(numaId), "Invalid NUMA node ID.");
} }
return NumaAllocNative(size, numaId); return NumaAllocNative(size, numaId);
@@ -69,7 +69,7 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
if (_numaNodes.Length == 0) { if (_numaNodes.Length == 0) {
throw new InvalidOperationException("Numa info has not been loaded."); throw new InvalidOperationException("Numa info has not been loaded.");
} }
if ((numaId < 0) || (numaId >= _numaNodes.Length)) { if ((numaId < 0) || !ContainsNode((uint)numaId)) {
throw new ArgumentOutOfRangeException(nameof(numaId), "Invalid NUMA node ID."); throw new ArgumentOutOfRangeException(nameof(numaId), "Invalid NUMA node ID.");
} }
NumaFreeNative(address, size, numaId); NumaFreeNative(address, size, numaId);
@@ -80,8 +80,10 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)]
public static partial int GetNumaEach( public static partial int GetNumaEach(
uint numaId, uint numaId,
Span<ulong> CpuIndexInNuma, Span<uint> processorGroupIds,
Span<uint> globalCpuIdInNuma Span<uint> socketIds,
Span<uint> cpuIndices,
Span<uint> numaNodeIds
); );
[LibraryImport(Native.LibName, EntryPoint = "nif_get_numa_overall")] [LibraryImport(Native.LibName, EntryPoint = "nif_get_numa_overall")]
@@ -97,10 +99,10 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
if (_numaNodes.Length == 0) { if (_numaNodes.Length == 0) {
throw new InvalidOperationException("Numa info has not been loaded."); throw new InvalidOperationException("Numa info has not been loaded.");
} }
L4[] buffers = new L4[NumaNodeCount]; L4[] buffers = new L4[_numaNodes.Length];
for (int i = 0; i < NumaNodeCount; i++) for (int i = 0; i < _numaNodes.Length; i++)
{ {
L4 buffer = new L4(i); L4 buffer = new L4((int)_numaNodes[i].NumaNodeId);
buffers[i] = buffer; buffers[i] = buffer;
} }
return buffers; return buffers;
@@ -108,14 +110,29 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
internal static void Initialize() internal static void Initialize()
{ {
Span<int> coreCountInNuma = stackalloc int[(Environment.ProcessorCount / 32) + 1];
int numaNodeCount = 0; int numaNodeCount = 0;
if (0 == GetNumaOverall(ref numaNodeCount, coreCountInNuma)) { if (0 == GetNumaOverall(ref numaNodeCount, default)) {
throw new InvalidOperationException("Failed to get NUMA overall information."); throw new InvalidOperationException("Failed to get NUMA overall information.");
} }
_numaNodes = new NumaNode[numaNodeCount]; int[] coreCountInNuma = new int[numaNodeCount];
for (uint i = 0; i < numaNodeCount; i++) { if (0 == GetNumaOverall(ref numaNodeCount, coreCountInNuma)) {
_numaNodes[i] = InitNumaNode(i, coreCountInNuma[(int)i]); throw new InvalidOperationException("Failed to get NUMA node processor counts.");
}
int activeNodeCount = 0;
for (int i = 0; i < coreCountInNuma.Length; i++) {
if (coreCountInNuma[i] > 0) {
activeNodeCount++;
}
}
NumaNodeCount = activeNodeCount;
_numaNodes = new NumaNode[activeNodeCount];
int nodeIndex = 0;
for (uint numaId = 0; numaId < numaNodeCount; numaId++) {
int coreCount = coreCountInNuma[(int)numaId];
if (coreCount == 0) {
continue;
}
_numaNodes[nodeIndex++] = InitNumaNode(numaId, coreCount);
} }
} }
@@ -123,8 +140,8 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)]
internal static partial void SetThreadAffinity( internal static partial void SetThreadAffinity(
int coreId, int cpuIndex,
int numaId int processorGroupId
); );
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
@@ -133,9 +150,24 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
int count int count
) )
{ {
Span<uint> globalIndex = stackalloc uint[count]; Span<uint> processorGroupIds = stackalloc uint[count];
_ = GetNumaEach(numaId, null, globalIndex); Span<uint> socketIds = stackalloc uint[count];
return new(numaId, count, globalIndex); Span<uint> cpuIndices = stackalloc uint[count];
Span<uint> numaNodeIds = stackalloc uint[count];
if (0 == GetNumaEach(numaId, processorGroupIds, socketIds, cpuIndices, numaNodeIds)) {
throw new InvalidOperationException($"Failed to enumerate processors for NUMA node {numaId}.");
}
return new(numaId, count, processorGroupIds, socketIds, cpuIndices, numaNodeIds);
}
private static bool ContainsNode(uint numaId)
{
foreach (NumaNode node in _numaNodes) {
if (node.NumaNodeId == numaId) {
return true;
}
}
return false;
} }
[LibraryImport(Native.LibName, EntryPoint = "nif_numa_alloc")] [LibraryImport(Native.LibName, EntryPoint = "nif_numa_alloc")]
@@ -22,16 +22,26 @@ namespace Nitload.Common.HighPerformance.ProcessorAbstraction
internal NumaNode( internal NumaNode(
uint numaId, uint numaId,
int count, int count,
Span<uint> logicalCoreIds Span<uint> processorGroupIds,
Span<uint> socketIds,
Span<uint> cpuIndices,
Span<uint> numaNodeIds
) )
{ {
NumaNodeId = numaId;
_logicalCores = new LogicalCore[count]; _logicalCores = new LogicalCore[count];
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
_logicalCores[i] = new LogicalCore(1, numaId, logicalCoreIds[i]); _logicalCores[i] = new LogicalCore(
processorGroupIds[i],
socketIds[i],
cpuIndices[i],
numaNodeIds[i]);
} }
} }
public uint NumaNodeId { get; }
public int LogicalCoreCount => _logicalCores.Length; public int LogicalCoreCount => _logicalCores.Length;
public ReadOnlySpan<LogicalCore> LogicalCores => _logicalCores; public ReadOnlySpan<LogicalCore> LogicalCores => _logicalCores;
+3 -3
View File
@@ -37,7 +37,7 @@ namespace Nitload.Threading
LogicalCore core LogicalCore core
) )
{ {
SetThreadAffinity(core.GlobalCpuId, core.NumaNodeId); SetThreadAffinity(core.CpuIndex, core.ProcessorGroupId);
} }
[Conditional("DEBUG")] [Conditional("DEBUG")]
@@ -55,8 +55,8 @@ namespace Nitload.Threading
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)]
private static partial void SetThreadAffinity( private static partial void SetThreadAffinity(
uint globalCoreId, uint cpuIndex,
uint numaNodeId uint processorGroupId
); );
#pragma warning restore CA5393 #pragma warning restore CA5393
+1 -1
View File
@@ -97,7 +97,7 @@ namespace Nitload
} }
Thread.Sleep(10); Thread.Sleep(10);
Numa.SetThreadAffinity((int)threadConfig.BindedCoreIndex, (int)threadConfig.BindedNumaNode); Numa.SetThreadAffinity((int)threadConfig.CpuIndex, (int)threadConfig.ProcessorGroupId);
while (mainProvider.IsRunning) while (mainProvider.IsRunning)
{ {
long elapsedTicks; long elapsedTicks;
@@ -14,15 +14,15 @@ using System.Threading;
namespace Nitload.Common.Timing namespace Nitload.Common.Timing
{ {
public class TimeSourceStartupConfiguration( public class TimeSourceStartupConfiguration(
nuint bindedNumaNode = 0, nuint processorGroupId = 0,
nuint bindedCoreIndex = 0, nuint cpuIndex = 0,
ThreadPriority threadPriority = ThreadPriority.Highest ThreadPriority threadPriority = ThreadPriority.Highest
) )
{ {
public nuint BindedNumaNode { get; } = bindedNumaNode; public nuint ProcessorGroupId { get; } = processorGroupId;
public nuint BindedCoreIndex { get; } = bindedCoreIndex; public nuint CpuIndex { get; } = cpuIndex;
public ThreadPriority Priority { get; } = threadPriority; public ThreadPriority Priority { get; } = threadPriority;
@@ -27,9 +27,13 @@ NIF_API SIMDID NIF_DECL nif_get_simd_level_global();
NIF_API SIMDID NIF_DECL nif_get_simd_level_local(); NIF_API SIMDID NIF_DECL nif_get_simd_level_local();
NIF_API SIMDID NIF_DECL nif_get_unite_simd(SIMDID left, SIMDID right); NIF_API SIMDID NIF_DECL nif_get_unite_simd(SIMDID left, SIMDID right);
NIF_API int NIF_DECL nif_compare_simd(SIMDID left, SIMDID right); NIF_API int NIF_DECL nif_compare_simd(SIMDID left, SIMDID right);
NIF_API void NIF_DECL nif_set_thread_affinity(int core_id, int numa_id); NIF_API void NIF_DECL nif_set_thread_affinity(int cpu_index, int processor_group_id);
NIF_API void* NIF_DECL nif_numa_alloc(size_t size, int numa_id); NIF_API void* NIF_DECL nif_numa_alloc(size_t size, int numa_id);
NIF_API void NIF_DECL nif_numa_free(void *ptr, size_t size, int numa_id); NIF_API void NIF_DECL nif_numa_free(void *ptr, size_t size, int numa_id);
NIF_API int NIF_DECL nif_get_numa_each(int numa_id, uint64_t *internal, uint32_t *global); NIF_API int NIF_DECL nif_get_numa_each(int numa_id,
uint32_t *processor_group_ids,
uint32_t *socket_ids,
uint32_t *cpu_indices,
uint32_t *numa_node_ids);
NIF_API int NIF_DECL nif_get_numa_overall(int *out_numa_count, int *count_of_each_numa); NIF_API int NIF_DECL nif_get_numa_overall(int *out_numa_count, int *count_of_each_numa);
#endif #endif
+56 -16
View File
@@ -27,8 +27,11 @@ typedef struct nif_linux_max_context {
} nif_linux_max_context; } nif_linux_max_context;
typedef struct nif_linux_fill_cpu_context { typedef struct nif_linux_fill_cpu_context {
uint64_t *internal; uint32_t *processor_group_ids;
uint32_t *global; uint32_t *socket_ids;
uint32_t *cpu_indices;
uint32_t *numa_node_ids;
int numa_id;
int index; int index;
} nif_linux_fill_cpu_context; } nif_linux_fill_cpu_context;
@@ -118,14 +121,37 @@ static int nif_linux_track_max_id(int value, void *context)
return 1; return 1;
} }
static int nif_linux_read_socket_id(int cpu_id, uint32_t *socket_id)
{
char path[160];
char buffer[64];
char *end = NULL;
long parsed_value;
snprintf(path, sizeof(path),
"/sys/devices/system/cpu/cpu%d/topology/physical_package_id", cpu_id);
if (!nif_linux_read_first_line(path, buffer, sizeof(buffer)))
return 0;
errno = 0;
parsed_value = strtol(buffer, &end, 10);
if (end == buffer || errno == ERANGE || parsed_value < 0 || parsed_value > UINT32_MAX)
return 0;
*socket_id = (uint32_t)parsed_value;
return 1;
}
static int nif_linux_fill_cpu_id(int value, void *context) static int nif_linux_fill_cpu_id(int value, void *context)
{ {
nif_linux_fill_cpu_context *fill_context = (nif_linux_fill_cpu_context *)context; nif_linux_fill_cpu_context *fill_context = (nif_linux_fill_cpu_context *)context;
uint32_t socket_id = 0;
if (!nif_linux_read_socket_id(value, &socket_id))
return 0;
if (fill_context->internal != NULL) fill_context->processor_group_ids[fill_context->index] = 0u;
fill_context->internal[fill_context->index] = (uint64_t)fill_context->index; fill_context->socket_ids[fill_context->index] = socket_id;
if (fill_context->global != NULL) fill_context->cpu_indices[fill_context->index] = (uint32_t)value;
fill_context->global[fill_context->index] = (uint32_t)value; fill_context->numa_node_ids[fill_context->index] = (uint32_t)fill_context->numa_id;
fill_context->index++; fill_context->index++;
return 1; return 1;
@@ -211,12 +237,14 @@ static int nif_linux_populate_writable_mapping(void *address, size_t size)
#endif #endif
} }
/* Pin the calling thread to a single logical processor. /* Linux has no Windows-style processor groups; processor_group_id is ignored. */
* core_id >= 0 : bind to that logical processor (global index, across groups). NIF_API void NIF_DECL nif_set_thread_affinity(int cpu_index, int processor_group_id)
* core_id < 0 : clear affinity constraint within the current group. */
NIF_API void NIF_DECL nif_set_thread_affinity(int core_id, int numa_id)
{ {
int cpu_count = (int)sysconf(_SC_NPROCESSORS_ONLN); (void)processor_group_id;
int cpu_count = (int)sysconf(_SC_NPROCESSORS_CONF);
if (cpu_index >= cpu_count)
cpu_count = cpu_index + 1;
if (cpu_count <= 0) if (cpu_count <= 0)
return; return;
@@ -227,11 +255,11 @@ NIF_API void NIF_DECL nif_set_thread_affinity(int core_id, int numa_id)
CPU_ZERO_S(size, set); CPU_ZERO_S(size, set);
if (core_id < 0) { if (cpu_index < 0) {
for (int i = 0; i < cpu_count; i++) for (int i = 0; i < cpu_count; i++)
CPU_SET_S((int)i, size, set); CPU_SET_S((int)i, size, set);
} else { } else {
CPU_SET_S(core_id, size, set); CPU_SET_S(cpu_index, size, set);
} }
pthread_setaffinity_np(pthread_self(), size, set); pthread_setaffinity_np(pthread_self(), size, set);
@@ -264,9 +292,14 @@ NIF_API int NIF_DECL nif_get_numa_overall(int *out_numa_count, int *count_of_eac
return 1; return 1;
} }
NIF_API int NIF_DECL nif_get_numa_each(int numa_id, uint64_t *internal, uint32_t *global) NIF_API int NIF_DECL nif_get_numa_each(int numa_id,
uint32_t *processor_group_ids,
uint32_t *socket_ids,
uint32_t *cpu_indices,
uint32_t *numa_node_ids)
{ {
if (numa_id < 0) if (numa_id < 0 || processor_group_ids == NULL || socket_ids == NULL ||
cpu_indices == NULL || numa_node_ids == NULL)
return 0; return 0;
char path[128]; char path[128];
@@ -275,7 +308,14 @@ NIF_API int NIF_DECL nif_get_numa_each(int numa_id, uint64_t *internal, uint32_t
if (!nif_linux_read_first_line(path, buffer, sizeof(buffer))) if (!nif_linux_read_first_line(path, buffer, sizeof(buffer)))
return 0; return 0;
nif_linux_fill_cpu_context fill_context = { internal, global, 0 }; nif_linux_fill_cpu_context fill_context = {
processor_group_ids,
socket_ids,
cpu_indices,
numa_node_ids,
numa_id,
0
};
return nif_linux_visit_id_list(buffer, nif_linux_fill_cpu_id, &fill_context); return nif_linux_visit_id_list(buffer, nif_linux_fill_cpu_id, &fill_context);
} }
+135 -36
View File
@@ -3,33 +3,102 @@
#if NIF_IS_OS(NIF_OS_WINDOWS) #if NIF_IS_OS(NIF_OS_WINDOWS)
/* Pin the calling thread to a single logical processor. static int nif_windows_get_socket_map(uint32_t **out_socket_map, DWORD *out_group_count)
* core_id >= 0 : bind to that logical processor (global index, across groups).
* core_id < 0 : clear affinity constraint within the current group. */
NIF_API void NIF_DECL nif_set_thread_affinity(int core_id, int numa_id)
{ {
if (core_id < 0 || numa_id < 0) { DWORD group_count = GetActiveProcessorGroupCount();
/* Query which group the thread currently belongs to */ DWORD buffer_size = 0;
uint32_t *socket_map;
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *information;
BYTE *cursor;
uint32_t socket_id = 0;
if (group_count == 0)
return 0;
socket_map = (uint32_t *)malloc((size_t)group_count * 64u * sizeof(uint32_t));
if (socket_map == NULL)
return 0;
for (size_t i = 0; i < (size_t)group_count * 64u; i++)
socket_map[i] = UINT32_MAX;
GetLogicalProcessorInformationEx(RelationProcessorPackage, NULL, &buffer_size);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || buffer_size == 0) {
free(socket_map);
return 0;
}
information = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *)malloc(buffer_size);
if (information == NULL ||
!GetLogicalProcessorInformationEx(RelationProcessorPackage, information, &buffer_size)) {
free(information);
free(socket_map);
return 0;
}
cursor = (BYTE *)information;
while (cursor < (BYTE *)information + buffer_size) {
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *entry =
(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *)cursor;
if (entry->Relationship == RelationProcessorPackage) {
PROCESSOR_RELATIONSHIP *package = &entry->Processor;
for (WORD i = 0; i < package->GroupCount; i++) {
WORD group = package->GroupMask[i].Group;
KAFFINITY mask = package->GroupMask[i].Mask;
if (group >= group_count)
continue;
for (DWORD cpu = 0; cpu < 64; cpu++) {
if ((mask & ((KAFFINITY)1 << cpu)) != 0)
socket_map[(size_t)group * 64u + cpu] = socket_id;
}
}
socket_id++;
}
if (entry->Size == 0)
break;
cursor += entry->Size;
}
free(information);
*out_socket_map = socket_map;
*out_group_count = group_count;
return 1;
}
static int nif_windows_get_processor_node(WORD group, BYTE cpu, USHORT *node_id)
{
PROCESSOR_NUMBER processor;
memset(&processor, 0, sizeof(processor));
processor.Group = group;
processor.Number = cpu;
return GetNumaProcessorNodeEx(&processor, node_id) != 0;
}
/* Pin the calling thread using its processor group and group-local CPU index. */
NIF_API void NIF_DECL nif_set_thread_affinity(int cpu_index, int processor_group_id)
{
if (cpu_index < 0 || processor_group_id < 0) {
GROUP_AFFINITY current; GROUP_AFFINITY current;
GetThreadGroupAffinity(GetCurrentThread(), &current); GetThreadGroupAffinity(GetCurrentThread(), &current);
/* Build a full mask for that group's actual core count */
DWORD count = GetActiveProcessorCount(current.Group); DWORD count = GetActiveProcessorCount(current.Group);
GROUP_AFFINITY ga; GROUP_AFFINITY affinity;
memset(&ga, 0, sizeof(ga)); memset(&affinity, 0, sizeof(affinity));
ga.Group = current.Group; affinity.Group = current.Group;
ga.Mask = (count >= 64) ? ~(KAFFINITY)0 : ((KAFFINITY)1 << count) - 1; affinity.Mask = (count >= 64) ? ~(KAFFINITY)0 : ((KAFFINITY)1 << count) - 1;
SetThreadGroupAffinity(GetCurrentThread(), &ga, NULL); SetThreadGroupAffinity(GetCurrentThread(), &affinity, NULL);
return; return;
} }
int index = core_id % 64;
DWORD count = GetActiveProcessorCount(numa_id); if ((DWORD)processor_group_id >= GetActiveProcessorGroupCount() || cpu_index >= 64)
if ((DWORD)index >= count)
return; return;
GROUP_AFFINITY ga; USHORT node_id = 0;
memset(&ga, 0, sizeof(ga)); if (!nif_windows_get_processor_node((WORD)processor_group_id, (BYTE)cpu_index, &node_id))
ga.Group = (WORD)numa_id; return;
ga.Mask = (KAFFINITY)1 << index;
SetThreadGroupAffinity(GetCurrentThread(), &ga, NULL); GROUP_AFFINITY affinity;
memset(&affinity, 0, sizeof(affinity));
affinity.Group = (WORD)processor_group_id;
affinity.Mask = (KAFFINITY)1 << cpu_index;
SetThreadGroupAffinity(GetCurrentThread(), &affinity, NULL);
} }
NIF_API int NIF_DECL nif_get_numa_overall(int *out_numa_count, int *count_of_each_numa) NIF_API int NIF_DECL nif_get_numa_overall(int *out_numa_count, int *count_of_each_numa)
@@ -39,30 +108,60 @@ NIF_API int NIF_DECL nif_get_numa_overall(int *out_numa_count, int *count_of_eac
return 0; return 0;
*out_numa_count = (int)highest + 1; *out_numa_count = (int)highest + 1;
for (UCHAR n = 0; n <= (UCHAR)highest; n++) { if (count_of_each_numa != NULL) {
ULONGLONG mask = 0; for (ULONG n = 0; n <= highest; n++)
GetNumaNodeProcessorMask(n, &mask); count_of_each_numa[n] = 0;
count_of_each_numa[n] = (int)__popcnt64(mask); }
DWORD group_count = GetActiveProcessorGroupCount();
for (DWORD group = 0; group < group_count; group++) {
for (DWORD cpu = 0; cpu < 64; cpu++) {
USHORT node_id = 0;
if (nif_windows_get_processor_node((WORD)group, (BYTE)cpu, &node_id) &&
count_of_each_numa != NULL && node_id <= highest)
count_of_each_numa[node_id]++;
}
} }
return 1; return 1;
} }
NIF_API int NIF_DECL nif_get_numa_each(int numa_id, uint64_t *internal, uint32_t *global) NIF_API int NIF_DECL nif_get_numa_each(int numa_id,
uint32_t *processor_group_ids,
uint32_t *socket_ids,
uint32_t *cpu_indices,
uint32_t *numa_node_ids)
{ {
ULONGLONG mask = 0; if (numa_id < 0 || processor_group_ids == NULL || socket_ids == NULL ||
if (!GetNumaNodeProcessorMask((UCHAR)numa_id, &mask)) cpu_indices == NULL || numa_node_ids == NULL)
return 0; return 0;
int intra = 0; uint32_t *socket_map = NULL;
while (mask) { DWORD group_count = 0;
DWORD idx; if (!nif_windows_get_socket_map(&socket_map, &group_count))
_BitScanForward64(&idx, mask); return 0;
if (internal != NULL)
internal[intra] = (uint64_t)intra; int index = 0;
global[intra] = (uint32_t)idx; for (DWORD group = 0; group < group_count; group++) {
intra++; for (DWORD cpu = 0; cpu < 64; cpu++) {
mask &= mask - 1; USHORT node_id = 0;
if (!nif_windows_get_processor_node((WORD)group, (BYTE)cpu, &node_id) ||
node_id != (USHORT)numa_id)
continue;
uint32_t socket_id = socket_map[(size_t)group * 64u + cpu];
if (socket_id == UINT32_MAX) {
free(socket_map);
return 0;
} }
processor_group_ids[index] = (uint32_t)group;
socket_ids[index] = socket_id;
cpu_indices[index] = cpu;
numa_node_ids[index] = node_id;
index++;
}
}
free(socket_map);
return 1; return 1;
} }
@@ -0,0 +1,49 @@
// -----------------------------------------------------------------------------
// file="Mix"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nitload.RegularAnalysis.Abstraction
{
public class Mix<TElement, TValue, TEvaluator>(
TEvaluator evaluator
) where TEvaluator : IMixEvaluator<TElement, TValue>
{
public TValue Weight { get; private set; } = default!;
private TEvaluator Evaluator { get; init; } = evaluator;
public TValue Evaluate(
Span<Relation<TElement, TValue>> relations
)
{
Weight = Evaluator.Evaluate(relations);
return Weight;
}
}
public sealed class Mix<TElement, TValue>(
IMixEvaluator<TElement, TValue> evaluator
) : Mix<TElement, TValue, IMixEvaluator<TElement, TValue>>(evaluator) { }
public interface IMixEvaluator<TElement, TValue>
{
public Relation<TElement, TValue, IRelationEvaluator<TElement, TValue>>[] DownstreamPorts { get; }
TValue Evaluate(
Span<Relation<TElement, TValue>> relations
);
}
}
@@ -0,0 +1,47 @@
// -----------------------------------------------------------------------------
// file="Relation"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nitload.RegularAnalysis.Abstraction
{
public class Relation<TElement, TValue, TEvaluator>(
TEvaluator evaluator
) where TEvaluator : IRelationEvaluator<TElement, TValue>
{
public TEvaluator Evaluator { get; init; } = evaluator;
public TValue Weight { get; private set; } = default!;
public TValue Evaluate(
ReadOnlySpan<TElement> sequence
)
{
Weight = Evaluate(sequence);
return Weight;
}
}
public class Relation<TElement, TValue>(
IRelationEvaluator<TElement, TValue> evaluator
) : Relation<TElement, TValue, IRelationEvaluator<TElement, TValue>>(evaluator) { }
public interface IRelationEvaluator<TElement, TValue>
{
TValue Evaluate(
ReadOnlySpan<TElement> sequence
);
}
}
@@ -0,0 +1,341 @@
# NGRA-Tensor 设计讨论记忆
更新日期:2026-09-18
目标项目:`D:\platform-dev\src\Nitload.RegularAnalysis`
来源:关于正则分析、NFA、Transformer、GPU 执行与 NGRA 双算子设计的讨论。
> 本文件记录设计意图与讨论结果,不代表项目现有代码已经实现这些功能。本次仅检查了目标目录及祖先目录的 AGENTS.md,没有审计源码。下文区分用户已提出的设计决定、辅助建议与待验证问题。
最新设计补充(2026-09-18,详见第 10 节):NGRA 使用持久状态上的增量流输入/输出;以连接占用作为主要容量指标的候选方案;容量不足时丢弃低激活连接。**字典本身是结构的一部分,结构是记忆的一部分。** 字典与运行时实例的区分属于身份与生命周期管理,不表示字典位于记忆系统之外。
## 1. 项目目标与讨论方式
NGRA-Tensor 试图结合结构化关系、模糊特征识别、多候选解释及模式预测。出发点包含正则模式分析与 NFA,但不应预设必须逐节点模拟 NFA 的执行方式。
核心工程矛盾:细粒度、动态、自组织的关系网络表达灵活,却容易产生大量小操作、串行依赖、零碎访存和复杂调度,影响 GPU 利用率与训练。
用户希望学习“如何构造能运行且高效的系统”,而不只是阅读既有模型的前向公式或事后解释。后续讨论应说明:需求 → 设计选择 → 数学表达 → 执行方式 → 代价 → 验证方法。不要用 Transformer 也存在串行步骤,来回避 NGRA 的节点过小与调度问题。
“Tr”在不影响理解时作为 Transformer 的简称。
## 2. 数学符号约定
| 符号 | 含义 |
| --- | --- |
| `Scal` | 标量 |
| `Vec_d` | d 维向量,本文使用列形式表示分量 |
| `Covec_d` | 对向量进行线性求值的余向量 |
| `Mat_x_y` | 每行 x 个元素、每列 y 个元素,即 y 行、x 列;列数在前 |
| `Tens_u_v` | 接收 u 个 Vec 和 v 个 Covec、输出 Scal 的多重线性对象 |
注意:
- 数组的存储维数不等于 `Tens_u_v` 的槽位数。ONNX 等工具中的 tensor 通常只是多维数组的名称。
- 线性映射 L 可通过 `T(x, φ) = φ(Lx)` 对应 `Tens_1_1`;输入向量与输出余向量可以属于不同维度的空间。
- 点积所对应的度量可视为 `Tens_2_0`。将向量与余向量识别需要相应的度量约定,不能只凭行列形状混为一谈。
- 带偏置映射一般为仿射映射;softmax、ReLU、LayerNorm 等不是多重线性运算。完整神经网络不能直接按上述定义称为一个 `Tens_u_v`。
## 3. 用户已提出的核心架构决定
### 3.1 两种组合算子
**relation:评估一定数量的 notation 按顺序串接形成的关系。**
```text
R = Relation(N1, N2, ..., Np)
```
port 表达有序关系,交换 port 输入通常会改变含义。此前使用过 `R_6` 表示 64 口 relation 的约定;64 指串行关系中的输入数量,不是特征向量维数。早期等级与端口数使用过 `2^L` 的联系,但它与新架构传播层级的准确对应仍须正式定义。
**notation(亦讨论为 idea):组织若干并列 relation。**
```text
N = Notation{R1, R2, ..., Rk}
```
这为多种可能结构提供显式位置,避免把并列候选强行塞进 relation 的串行 port。
- 两者底层数据结构可以形态一致,但必须保留类型及不同的运算语义。
- 并列候选不自动等于语义独立,也不自动等于互斥。互斥解释与可以同时成立的特征,需要不同的组合规则。
- 候选评估、候选权重、候选混合是可区分的操作;不必一生成候选就压成一个 Vec。
### 3.2 双向分层与反复传播
用户提出的方向:
```text
relation 方向:L0 → L1 → ... → L6
notation 方向:N6 → N5 → ... → N0
一次推理可能经历多轮上述传播。
```
这是当前设计意图,尚不是完整的状态更新规范。L/N 等级的含义、跨级连接是否合法、上下两个方向如何连接,都需要进一步明确。
讨论中提出的一种解释(待落实):
- 上行:由局部证据评估更大的组合与解释。
- 下行:较高层的解释、约束和关注影响局部候选的保留与活跃程度。
- notation 的候选评估仍须汇集,才能为高层提供依据;“notation 向下传播”不能遗漏候选证据如何上传。
- 区分来自输入的证据与来自上层的反馈,防止反馈被反复当成独立证据累加,产生自我强化。
### 3.3 统一机器字典与索引空间
用户决定让 token、relation、notation 位于同一个字典和索引空间。“字”本身也是一种原子理念。
- token 使用预存字典提供初始表示。
- relation、notation 的实例“词义”通过节点计算特征向量得到。
- 字典可以保存结构定义、参数及引用;运行时计算得到本次语境中的实例特征。
- 预存 token 特征不必永久固定为运行时最终特征,可以接受上下文更新。
建议明确区分两层身份:
| 层面 | 内容 |
| --- | --- |
| 字典条目 | token/关系/概念的定义、类型、结构与参数 |
| 运行时实例 | 引用哪个条目、出现在何处、连接哪些实例、本轮状态与得分 |
同一个字在文本中出现两次,可以共享字典 ID,但不能因此共享同一份位置相关状态。可采用“实例 ID + 字典引用”的实现,不要求推翻统一字典。
## 4. 每轮输出:面向下一轮的规范形
用户提出:一次“小 node → 大 node → 小 node”的传播后,输出一种较工整、供下一轮继续处理的状态。
示例:
```text
输入:吃饭了吗,您?
初步规范表达:我,提问,你,吃饭,状态
```
各状态同时具有特征向量,用于表达语气等细节。
这里建议把“规范形”理解为结构统一的语义中间表示,暂不要求第一轮得到唯一、最终正确的解释。
### 4.1 必须保留角色绑定
仅有上述扁平序列,不能明确区分“我问你是否吃饭”和“我问你我是否吃饭”。一个可参考的结构为:
```text
提问
├─ 发起者:当前说话者(来自会话上下文)
├─ 接收者:您所指的人
└─ 内容:进食事件是否已经发生
├─ 行为者:您所指的人
├─ 行为:吃饭
└─ 发生状态:待确认
```
这是讨论中的建议示例,不是已决定的 IR 格式。
- “我”没有直接出现在原句,应记录其上下文来源。
- 被询问的事件状态不能在规范化时变成已成立的事实。
- 敬称、语气等细节可通过特征表示;关键角色关系应有明确结构。
- 返回小节点时,单位可以是实体引用、行为、状态变量或关系实例,不必恢复成原始字符。
### 4.2 保留候选与来源
“询问进食情况”和“以此寒暄”可能是候选解释,也可能兼有。notation 应能保留候选身份、得分与结构,而不是强制提前选唯一结果。
建议保留输入片段或上下文依据,区分:显式内容、上下文补足、模型推断、待确认状态。
### 4.3 轮次与稳定性
可用以下关系描述一次传播,但具体更新函数尚未确定:
```text
S(t) → 组合与解释评估 → H(t) → 角色绑定、反馈与整理 → S(t+1)
```
建议每轮读取上一轮状态并集中形成下一轮状态,避免执行顺序无意改变语义。最终应定义最大轮数、变化阈值或其他停止条件。
期望已稳定的表示在没有新证据时满足 `F(S*) ≈ S*`;这只是验证目标,不代表目前已证明收敛。
## 5. 构建、剪枝与训练:尚未完成的设计
用户明确指出,当前“葫芦形”传播还不够明确,需要剪枝和额外构建规则。
优先需要确定:
1. relation 各 port 的角色、输入类型、顺序与允许的连接范围。
2. notation 中哪些候选回答同一个解释问题,是否覆盖相同区间或具有相容的输出类型。
3. 候选是互斥、可并存还是具有其他依赖;采用何种打分与归一化。
4. 构建网络、选择路径与更新特征,哪些是离散决策,哪些可微、哪些需要额外训练机制。
5. 如何限制候选数量,并测量剪枝造成的正确解释丢失。
6. 如何防止重复证据、自我支持、循环振荡及无上限的关系扩张。
7. 如何保留原始词序、实体身份、否定、疑问、范围及来源。
简单 regex/NFA 的布尔关系可以用向量、矩阵及相应的逻辑运算表达;转成浮点矩阵后,必须重新定义多路径的组合含义。布尔可达性、概率、模糊兼容度与特征混合不可直接互换。
目前没有得到“任意一组 NFA 能以常数总计算量执行”的结论。并行深度、总工作量、状态规模和存储量必须分别分析。
## 6. GPU 执行路线
### 6.1 语义粒度与执行粒度分离
讨论中形成的总体判断:
- 常规稠密 Tr 从规则的大计算出发,再为硬件分块、融合与安排数据复用。
- NGRA 从细粒度的关系与候选出发,再把已经就绪、形状相容的操作合并为批次。
不必让每个小 relation 对应一次 GPU 调用,也不必为批处理把语义上的多个 relation 合并成一个大 relation。
| 方法 | 收益与限制 |
| --- | --- |
| 批处理独立实例 | 可保持实例语义,同时提高单次执行规模 |
| 融合有依赖的操作 | 可减少启动及中间搬运;不会自动消除数据依赖 |
| 按层级、轮次、形状分组 | 可以简化调度;具体规则尚待验证 |
| 稳定的数据布局与参数分组 | 有机会提高复用并减少碎片化读取 |
早期低阶、小 port 关系的串行活动密集,是重点瓶颈。把 L0/L1 放在 CPU、把短模式预筛编译成矩阵运算等,均只是候选方向,尚未通过基准测试决定。
### 6.2 按需加载模型内容
用户提出的潜在优势:根据激活情况,只加载本次需要的 relation/notation 参数,降低显存工作集。
成立条件与待验证因素:
- 用廉价、常驻的索引或路由信息判断加载对象,避免必须先读完完整参数才能决定不用它。
- 测量当前活跃集合、跨轮复用、加载粒度、预取能力及批次内激活集合的并集。
- 同时核算参数、活跃状态、候选记录、运行缓冲与传输缓冲。
- 省显存不自动等于提速;零碎且依赖刚完成计算的加载可能使 GPU 等待。
- 区分 GPU 内部缓存未命中与数据不驻留显存、需要从主存迁入。
Tr 也能参数卸载、逐层加载或使用 MoE,故不能把“必须所有权重常驻显存”当作 Tr 的数学要求。NGRA 的优势仍需在质量相当时实测。
## 7. Tr 对照:已经澄清的关键概念
### 7.1 block、上下文与执行分块不是一回事
- Tr block 通常指深度方向的一层计算,不是固定 4096 口的文本单元。
- 4096 是本讨论的示例长度,也可能是某个模型的上下文配置;不是 Tr 固有 I/O 宽度。
- 同一层的投影权重作用于特征轴,通常不含序列长度 n 这个参数维度。
- 支持长上下文还涉及位置机制、训练与资源,不能据此宣称任意短上下文模型可直接可靠处理 1M。
- 彼此隔离的文本窗口不会自动互通。共享模型权重不等于保留上一段输入。
### 7.2 输入与输出
以 512 维、8 个头、每头 64 维的示例来说:
```text
X: Mat_n_512 → 一个完整 Tr block → Y: Mat_n_512
```
每个输出列仍对应一个位置,但携带上下文整合后的特征。输出通常不是显式 relation 表,也不是 vocabulary 概率。只有第一层直接接收 embedding 与初始位置表示,后续层接收前层输出。
token embedding 提供初始向量,不能把后续位置向量理解成始终只有原始 token 的静态词义。
### 7.3 本文列向量约定下的主要运算
| 运算 | 形状或说明 |
| --- | --- |
| `Q = WQ X`, `K = WK X`, `V = WV X` | W 为 `Mat_512_64`;Q/K/V 为 `Mat_n_64` |
| `S = QᵀK / sqrt(64) + M` | `Mat_n_n`;行是查询,列是候选 |
| `A = softmax_rows(S)` | 对每个查询的允许候选统一归一化 |
| `H = V Aᵀ` | `Mat_n_64`,按位置混合内容 |
| 8 个 H 沿特征轴拼接 | `Mat_n_512` |
| 输出投影、残差、Norm、FFN、残差、Norm | 最终仍为 `Mat_n_512`;此顺序是原论文 post-norm 示例 |
参数 WQ/WK/WV 通常分层、分头拥有,并在所有位置复用。ONNX 的图级 initializer 存储位置不代表各层共享同一组参数。Q/K/V/A 是本次输入产生的中间结果,不是查表得到的固定权重。
### 7.4 混合不等于重排
- `W X`:特征投影,不移动 token 列。
- `V Aᵀ`:根据输入计算权重,允许多个输出读取同一来源。
- `X Π`:用置换矩阵重排位置。
- 标准注意力不默认执行“聚类相关 token → 打包 → 分组路由”。若需要这种行为,须明确设计路由过程。
- 两个局部组的输出可以按 `[B00h B01h]`、`[B00l B01l]` 重组后交给下一层。若 h/l 表示 token 子集,这属于改变窗口分组;若表示特征通道子集,则是另一种操作。
- 重组本身不做语义融合,下游计算才融合;需要保留位置身份与因果约束。
### 7.5 1M 上下文与跨计算块通信
全局注意力可以逻辑上覆盖整个上下文,执行时按块读取 K/V。对于两个分块 A/B,跨块关系直接来自 `QAᵀ KB` 与 `QBᵀ KA`;因果掩码限制允许的方向。
一次处理 4096 个查询,不代表这些查询只能访问 4096 个候选。它们可以分批访问全部允许的 K/V,并维护统一的 softmax 累加状态。
不能对各候选块独立 softmax 后简单平均。FlashAttention 使用分块与在线归一化执行精确注意力;跨设备方案如 Ring Attention 可以传递 K/V 块完成跨分片交互。
这不要求先分配完整的巨大注意力矩阵再把它切开。
### 7.6 成本与能力边界
- 全局注意力的配对计算为 `O(n²d)`;序列长度翻倍,该部分工作量约四倍,不代表端到端推理时间必然四倍。
- 固定窗口宽度 w 的局部注意力为 `O(nwd)`;固定层数的间接信息覆盖范围有限。
- 使用 KV cache 生成一个新 token 时,该 token 对历史的注意力计算随历史长度线性增长,而非重新计算完整历史的所有配对。
- KV cache 不会自动把上下文容量变成无限,且需要区分缓存、压缩记忆与原始历史可访问性。
- 普通前馈网络与关闭随机操作的 Tr 都可确定性执行。“DNN”通常表示深度神经网络,不能用来与 Tr 作“确定/非确定”分类。
- 普通网络通常训练权重与偏置,而非修改固定 ReLU 等激活函数本身。
- 注意力混合不自动保留完整候选结构,也不等于显式逻辑关系或可解释的置信度。
## 8. 下一阶段建议产物(尚未执行)
1. 明确统一字典条目与运行时实例的最小 schema。
2. 明确 relation/notation 的输入输出类型、候选组合与角色绑定规则。
3. 写出一个完整传播轮次的状态读取、写入与反馈语义。
4. 用“吃饭了吗,您?”制作可逐节点检查的示例,包括歧义保留与下一轮稳定性。
5. 定义候选数量上限、剪枝指标、最大轮数与停止条件。
6. 先建立语义正确的参考执行,再比较逐节点、批处理、融合和按需加载的性能。
7. 训练目标、路由学习方式、收敛性质以及相对 Tr 的性能优势均保持待验证状态。
## 9. 讨论中核对过的参考资料
- [Attention Is All You Need](https://arxiv.org/html/1706.03762v7):架构、注意力、复杂度与参数作用。
- [Neural Machine Translation by Jointly Learning to Align and Translate](https://arxiv.org/abs/1409.0473):按当前需要读取输入位置的早期注意力设计。
- [FlashAttention](https://arxiv.org/abs/2205.14135):分块、IO 优化与精确注意力。
- [Ring Attention](https://arxiv.org/abs/2310.01889):跨设备传递 K/V 块与长上下文。
- [Swin Transformer](https://arxiv.org/abs/2103.14030):局部窗口之间通过改变划分建立联系的实例。
- [Switch Transformers](https://arxiv.org/abs/2101.03961):选择性激活专家参数。
- [ZeRO-Inference](https://www.deepspeed.ai/2022/09/09/zero-inference.html):参数卸载、逐层加载与预取。
- [ONNX Concepts](https://onnx.ai/onnx/intro/concepts.html):计算图、节点与 initializer。
上述资料作为对照,不意味着 NGRA 已被证明等价于其中某种架构。
## 10. 2026-09-18:持久结构、增量流与连接容量
### 10.1 用户明确提出的设计方向
- 从正则引擎的流式增量输入思路演进:输入、输出都在上一次运行结果所保留的状态上继续进行。
- 不以每次重新输入完整历史文本作为基本执行语义;历史对当前的影响由已保留的结构及其状态承接。
- 容量很可能按连接数量或连接占用评估,而不是按 token 数量评估;具体度量尚未定稿。
- 容量不足时,通过丢弃低激活连接释放容量。这是已提出的方向,评分、阈值、删除语义与正确性规则尚未确定。
- 字典本身也是结构,结构本身也是记忆。记忆不只包含特征向量数值,还包括对象身份、连接、组合方式、候选与参数化定义。
### 10.2 建议的抽象接口(非已实现 API)
```text
(S[t+1], ΔO[t]) = Step(S[t], ΔI[t])
```
- `S[t]`:上次保留的结构、字典相关状态、实例特征、候选、待执行依赖及流位置等。
- `ΔI[t]`:新增输入;是否包含撤回、修改和外部反馈,需进一步定义。
- `ΔO[t]`:本次新增输出;若允许修订先前解释,需要明确输出更新或撤回协议。
- `S[t+1]`:完成必要传播与容量管理后保留的状态。
输入流步 t 与一次输入触发的内部上下传播轮次应分开编号;一次增量输入可能触发多轮关系更新。
### 10.3 增量失效规则
Roslyn 增量管线与因果 Tr 的缓存复用提供了讨论类比:依赖没有改变的结果可以复用。但 NGRA 必须按自己的依赖图定义失效,而不能只检查原始文字是否变化。
- 下游输入、上层反馈、候选集合、相关字典结构或参数的变化,都可能使实例状态失效。
- 新输入可能激活此前尚未选择的连接,因此只记录已经活跃的依赖可能不够;候选发现与再激活机制待设计。
- 双向反馈可以扩大受影响区域,增量输入不保证只需要少量增量计算。
- 纯函数结果复用、依赖版本与参数版本是实现候选,不是目前已经确定的数据结构。
### 10.4 连接预算与遗忘:需要进一步决定的内容
1. 预算计算活跃连接、全部保留连接、驻留显存的连接,还是三者分别统计?
2. 不同连接携带的特征、参数、元数据大小可能不同;是否采用连接数量加权或真实字节数作为物理预算?
3. “丢弃”表示暂停执行、移出显存、删除当前实例边,还是删除可持久化的字典结构?这些行为对记忆的影响不同。
4. 当前激活低不必然意味着未来无用或结构可删除;角色绑定、串行 port、待完成模式等所需连接应有明确的完整性规则。
5. 连接移除后,哪些实例需要重新评估?候选如何重新发现?还保留哪些历史依据?
6. 遗忘顺序不必等同于时间顺序;连接重要性的估计方法和低激活持续时间阈值尚待实验。
这些是辅助建议与待决问题,不覆盖用户已提出的“容量不足时优先丢弃低激活连接”方向。
### 10.5 容量与智能的关系:待验证假设
用户提出“越智能的模型可能上下文更快耗尽”:在相同连接预算下,若模型为同一输入识别并保留更多关系、候选和依赖,容量可能更快用尽。
这不是已证明的必然关系。更好的抽象、共享、合并与剪枝也可能降低连接需求。后续应测量单位输入的新增连接、保留连接、复用率、候选召回与任务质量,而不是直接把智能程度等同于连接消耗。
补充参考:[Roslyn Incremental Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#pipeline-model-design)。该资料只支持增量管线类比,不证明 NGRA 的依赖追踪或遗忘方案正确。
@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------------
// file="PropertyEvaluator"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using Nitload.RegularAnalysis.Abstraction;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nitload.RegularAnalysis.PropertyRegex
{
internal abstract class PropertyEvaluator<T> : IRelationEvaluator<T, PropertyMatchState>
{
public abstract PropertyMatchState Evaluate(
ReadOnlySpan<T> sequence
);
}
}
@@ -0,0 +1,32 @@
// -----------------------------------------------------------------------------
// file="PropertyMatchEngine"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using Nitload.RegularAnalysis.Abstraction;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nitload.RegularAnalysis.PropertyRegex
{
public class PropertyMatchEngine<T>
{
private readonly List<Mix<T, PropertyMatchState>> _nodes = [];
private readonly List<Relation<T, PropertyMatchState, PropertyEvaluator<T>>> _relations = [];
}
public class FrozenPropertyMatchEngine<T>
{
private readonly Mix<T, PropertyMatchState>[] _nodes;
private readonly Relation<T, PropertyMatchState, PropertyEvaluator<T>>[] _relations;
}
}
@@ -0,0 +1,17 @@
// -----------------------------------------------------------------------------
// file="PropertyMatchState"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nitload.RegularAnalysis.PropertyRegex
{
internal class PropertyMatchState { }
}
@@ -14,11 +14,385 @@ left boarder-> match group start //push stack, add group index
-> match group end //pop stack, set group end, build sub sequence -> match group end //pop stack, set group end, build sub sequence
---
# NGRA / NGRA-Tensor 设计讨论纪要
记录日期:2026-09-18。本文压缩本次讨论,作为研究与设计记录;未检查现有实现是否符合这些描述。以下区分设计意图、候选实现和待验证问题,不将讨论建议视为已经确定或验证的架构。无歧义时以 `tr` 指 Transformer。
## 1. NGRA 的背景与当前重点
- NGRA 的基础方向是把正则从字符序列推广到一般符号及结构化属性序列。
- 基本有限正则 DLR(Deterministic Limited Regex):面向定长输入元素,重复具有上限,不采用断言及前后瞻,处理选择、重复、组合。
- DLR-CP:加入 case 优先级,可通过后处理器或节点信息处理。case 排序与模糊匹配是不同问题。
- 属性正则(讨论中称 ALR):输入是含有限个属性的 Struct,通过属性布尔判定 λ 进行匹配。有限属性个数不自动意味着有限取值空间。
- 编译器前端可采用增量管线:字符 → token → 属性处理 → 更高层序列/region → AST;通过递归提取“岛屿”region,再交给下级 matcher。
- 当前重点是 NGRA-Tensor:以关联程度处理模糊序列,研究如何根据新输入建立、调整和组合 relation。模糊包括关键特征之间存在低关联内容,不仅是 case 优先级或字符编辑距离。
- DFA 自发现与这一方向有联系,但布尔自动机学习不能未经修改就用于任意加权、上下文相关的关系网络。
## 2. 统一词汇与理念系统:核心设计意图
字、词、短语、语义理念在计算接口上平等,均可存在于统一字典中,并通过向量表示;序列允许混合不同粒度的节点。
示例:
```text
原句:吃饭了么,您?
基本词序列:吃饭,了,么,逗号,您,问号
候选语义标准型:我,询问,你的,吃饭,状态
```
标准型是设计示例,不是已确定的规范语法或唯一正确解释。理念的可读标签不必直接等同于自然语言词义。需要能表达角色、顺序、指代、时间、否定与上下文条件;不能把理念集合当作无结构的词袋。
应区分字典条目与本次出现的实例:相同词条可以在不同位置、事件和角色中出现。统一字典不要求把这些实例状态合并。
需要区分三件事:
1. 词条/理念的长期向量和参数。
2. 本次出现的上下文状态、绑定与激活程度。
3. 本轮计算后创建的新实例,以及长期学习时登记的新词条。
创建下一波次实例不必扩充词典。将稳定、反复复用的组合固化为新理念,属于独立的结构学习问题。
## 3. relation 与 mixer 交替执行
用户澄清:relation 判定器与 mixer 都有很多;mixer 是分布式、可学习的局部计算单元,不是一个集中式全局裁决器。
```text
有序理念实例
→ 多个 relation 计算关系响应
→ 多个 mixer 选择、融合、抑制、重表示
→ 新的有序理念实例
→ 下一计算波次
```
- mixer 可保留输入、融合成抽象节点、降低响应或使其不再进入下一波。
- 关系响应除数值外必须有对象身份或结构对应关系;裸分数不能唯一确定输出含义。这些对应可以由参数及数据布局实现,不要求全部采用显式符号提案。
- relation 可以独立作为计算结构,不必属于词汇表;这是可选架构方向,尚未最终确定。
- 表面词可退出传播,功能由抽象节点接替。例如“吗”可转为疑问功能;但将“了吗”全部只压成疑问可能丢失“是否已发生”的区别。
- 大量单元提供容量;训练目标仍须约束应保留哪些区别、合并哪些等价表达。
- 确定的执行算法不等于正确或唯一的语义推理。可以确定地保留多个解释;不能通过强制选择来宣称消除了歧义。
之前讨论过“relation 输出带证据提案、mixer 做约束组装”的方案,它可作局部实现参考,但不是用户最终确认的集中式架构。
## 4. 波次调度、依赖与候选发现
既有思路:下游节点编译后知道依赖及调用约束,通过 GPU 加载并激活。动态架构下,需要补充运行时激活发现。
区分:
- 依赖拉取:已经决定执行某个 relation,需要哪些输入与参数。
- 激活发现:当前结果出现后,哪些下游 relation 值得成为候选。
- 就绪判定:候选是否已绑定足够、相容的输入。
编译期可以保留 relation 模板、输入槽位、参数需求与先后约束;运行时决定实例绑定、候选组合与执行波次。反向依赖索引可通知受影响的候选,relation 自身的依赖描述负责取数,两者可以并存。
调度候选时需区分实例/region,避免跨语境错误组合;上游激活不代表下游立即执行。预算应同时约束活跃连接、候选展开和部分匹配状态,而不只限制最终执行数。
GPU 原型可按运算类型批处理任务数组,批量构造下一波任务;逻辑节点激活不必对应独立 kernel 启动。稀疏算法需要计入索引、去重、同步和随机访存成本。
## 5. 记忆与预测性淘汰
目标是持续更新语义状态,以活跃节点/连接控制工作规模,而不是一直保存全部原始 token。
三类操作应分开:
1. 已由高层表示承载含义的中间状态:可以释放。
2. 当前低激活但仍可能有用的信息:可移入持久记忆,未来召回。
3. 永久删除:接受由此失去某些未来回答能力。
用户明确接受遗忘:不要求无限保留,缺少信息时可以承认无法确认。系统需避免以相似候选替代缺失事实;完全遗忘后未必知道自己曾知道,不宜总声称“我忘了”。
淘汰策略可以借鉴预测缓存,综合估计未来使用概率、遗漏损失、保留成本、召回成本与成功率。当前激活低不代表未来价值低。
持续抽象可能优于临时文本摘要,但摘要本身也可以承载抽象;关键区别是状态能否局部更新、按需召回、保存关系与来源。tr 也存在内部状态记忆/压缩变体,不能把文本摘要当成所有 tr 的唯一记忆方式。
## 6. 小型 tr 作为局部计算模块:候选方向
考虑由 NGRA 管理字典、记忆、候选和波次,小型注意力模块计算局部 relation/mix。多个实例可共享参数,通过任务向量和角色信息区分,不必每条关系配一套网络。
```text
当前状态 → 路由/召回候选 → 配对的 K/V → 局部计算 → 输出映射 → 下一波
```
潜在强链接可提前组织候选,但不能仅允许历史强关系进入,否则新关系难以被发现。候选组织是独立难点,局部 attention 不会自动解决。
若每轮有 m 个包、每包 k 个实例、维度 d,局部稠密注意力部分约为 O(m k² d);总成本还包括投影、前馈网络、路由、访存、包间通信和波次数。
必须明确局部 V 是输入证据,还是候选输出理念。前者混合后仍需输出映射;后者已把输出候选发现交给上游。
## 7. 已澄清的 Transformer 计算原理
标准自注意力并不把整个字典放入 V,也不要求上下文窗口大于词表。
设词表大小 N、上下文长度 L、隐藏维度 d:
| 对象 | 形状/作用 |
|---|---|
| 词嵌入表 E | N × d,所有词条的基础表示 |
| 上下文状态 H | L × d,各位置的上下文表示 |
| Q、K、V | 由上下文状态投影;自注意力中行对应位置 |
| 注意力权重 A | 每头在数学上为 L × L,受掩码约束 |
| 最终预测状态 h | d 维,生成时通常使用最后位置 |
| 输出矩阵 W_out | N × d,覆盖全词表 |
```text
A = softmax(Q Kᵀ / sqrt(d_k)) # 决定从各位置读取多少
O = A V # 已经实际读取并混合信息
多层注意力、前馈、残差等处理 → h
logits = W_out h + b # 对整个词表打分
p = softmax(logits) # 下一 token 概率
```
两个 softmax 作用不同:注意力 softmax 面向可访问的上下文位置;输出 softmax 面向词表。注意力不是仅安排“以后如何注意”,AV 当场执行读取。
上下文未出现的词仍在 W_out 中有对应行,所以可以获得分数。训练同时调整前面的网络与输出映射,使合适上下文提高正确输出的概率。输入、输出嵌入可以共享权重,但不意味着输出层等于注意力 V。
上下文是生成条件,不是随机种子;随机采样种子影响如何从分布选择。新 token 加入上下文后,后续步骤再利用它。
有限字典不意味着无限长度序列的集合有限;有限属性个数不意味着各属性取值有限。生成未出现符号的能力来自可学习输出映射,不依赖“窗口覆盖字典”或“符号空间封闭”的解释。
注意力权重是当前层/头的相对读取权重,不宜直接视为永久关系强度或事实可信度。模型能力也分布在前馈、投影等参数中。上下文状态、KV 缓存与注意力激活通常不是可训练参数。
注意力不会保证全面、充分、正确地理解全部联系。多个 block 通常具有各自参数,不能无条件视为同一计算的循环。
## 8. 训练、生成与事实约束
从“相关”到“可推导”需要关系类型、方向、角色和组合规则。强边不自动具有传递性;向量检索适合召回候选,不能独自证明结论。
用户的事实定义:训练/前文中被接受为真,并经逻辑检验支持的信息。实现时仍需区分前提来源、适用条件与推导;内部一致不等于外部真实。
生成不是理解过程的简单求逆。可探索:问题与记忆 → 回答标准型/内容计划 → 混合理念与词的逐步展开 → 自然语言。需要跟踪角色、时态、否定、覆盖与依据,避免风格润色新增事实。
内容规划、语义到文本生成、忠实性检查是可分开训练和评估的任务。这是候选方案,不要求必须使用符号规划器。回译标准型可检查一致性,但共享错误使其不能成为唯一验证。
训练至少涉及:
- 表示与预测:下一节点、解释序列或其他明确任务。
- 参数学习:relation/mixer 的匹配和组合行为。
- 结构学习:新增、拆分、合并、删除关系及理念。
- 路由学习:有限预算下召回有用候选,并为新关系保留发现机会。
- 记忆学习:保留、休眠、淘汰与召回。
- 反例训练:错配人物、日期、否定、确定程度以及缺乏依据的组合。
tr 的进步同时依赖数据质量与规模、容量、后训练、可验证奖励、推理计算和工具;不能将连贯性或事实性提升仅归因于 QKV 或语料量。流畅、自洽与事实正确需要分开评价。
## 9. 当前判断与待验证事项
NGRA-Tensor 的组成思想有研究基础,但完整架构尚未被证明能训练通用 LLM。也不能据此断言它只适合典型模式,或必然缺少长程理解与自然交互能力。
优先做小型闭环:输入 → relation/mixer → 输出理念/序列 → 损失 → 更新;验证未见组合泛化、候选遗漏、信息遗忘与实际效率。
关键开放问题:
1. relation 与 mixer 的精确定义、参数共享和输出接口。
2. 多个 mixer 输出的顺序、实例绑定、冲突及重复处理。
3. 活跃集合外的新关系如何获得训练机会。
4. 向量如何映射到已有理念,何时登记新词条。
5. 离散激活/淘汰/结构变更的训练方式。
6. 弱证据如何积累,休眠信息如何被后续线索唤起。
7. 在同等质量下,端到端速度、显存和训练成本是否优于基线。
## 10. 讨论中相关文献
以下是可借鉴的组成部分,不表示这些系统已实现完整 NGRA-Tensor:
- [Transformer 原论文](https://arxiv.org/html/1706.03762v7):QKV、前馈网络与词表输出层。
- [SoPa](https://aclanthology.org/P18-1028/):可学习软序列模式;原始模式拓扑受限。
- [Rational Recurrences](https://aclanthology.org/D18-1152/):加权自动机与循环状态更新、语言建模。
- [流式概率自动机学习](https://borjaballe.github.io/papers/preprint-bcg13.pdf):增量状态发现与合并。
- [TensorLog](https://arxiv.org/abs/1605.06523)、[Neural LP](https://arxiv.org/abs/1702.08367):可微关系推理与规则学习。
- [NBFNet](https://arxiv.org/abs/2106.06935):路径上的关系组合和多路径汇总。
- [RetoMaton](https://arxiv.org/abs/2201.12431):通过自动机连接辅助语言模型检索。
- [Step-by-Step](https://aclanthology.org/N19-1236/):内容规划与语言实现分离。
- [DataTuner](https://aclanthology.org/2020.coling-main.218/):生成与语义忠实性重排序。
- [Transformer-XL](https://aclanthology.org/P19-1285/)、[Compressive Transformer](https://arxiv.org/abs/1911.05507):跨片段状态与内部记忆压缩。
- [Mamba](https://arxiv.org/abs/2312.00752)、[RWKV](https://arxiv.org/abs/2305.13048):其他可训练序列架构。
- [FlashAttention](https://arxiv.org/abs/2205.14135):精确注意力的访存优化,与减少候选关系不同。
## 11. 自动机理论:从 DLR 到可学习关系计算
本节整理讨论中的理论联系,并补齐理解公式所需的定义;不把这些形式化直接认作 NGRA 当前实现。
### 11.1 有限语言、正则语言与有限状态不是同一个概念
设有限字母表为 Σ,普通 DFA 的转移为 δ: Q × Σ → Q,识别语言 L ⊆ Σ*。DFA 状态有限,但识别的语言可以包含无限多个串,例如 a*。
若 DLR 同时满足有限输入字母表、所有重复有上限,且没有额外引入无界递归,则允许的串长具有有限上限 B,语言也是有限集:
```text
L ⊆ Σ^{≤B}
|Σ^{≤B}| = Σ_{k=0}^{B} |Σ|^k
```
这不代表状态数或学习成本必然小。递归 region 是另一层机制:局部有限匹配器配合可增长的栈/递归结构后,整体不能不加说明地继续视为一个固定有限状态机。
属性匹配可把字母表推广为 Struct,并用谓词检查输入。有限个字段不意味着有限个可能的 Struct;若要归约到普通有限字母表,需要有限取值或可用的有限等价分类。UTF-16 code unit 是定长单元,但不等于每个 Unicode 字符或字素都只占一个单元。
原文件顶部的 lookaround 草稿被保留作历史内容;它与本次 DLR“不采用断言/前后瞻”的约束不应未经核实就合并为一份最终规范。
### 11.2 自动机自发现的关键是区分后续行为
对于语言 L,两个历史前缀 u、v 在所有后缀上具有相同接受行为时,可以属于同一右同余类:
```text
u ~_L v ⇔ 对所有 z∈Σ*,uz∈L 当且仅当 vz∈L
```
这提供了“何时需要不同状态”的理论视角。样本学习只能观察有限后缀,因此是否合并还依赖样本覆盖、查询能力、统计假设或归纳偏好。
- RPNI 类方法:利用正反例,尝试合并前缀树状态,并避免与已知标签冲突。
- ALERGIA/流式 PDFA 学习:比较后续概率行为的统计兼容性,决定区分或合并。
- 从有限样本得到一个一致模型,不代表已经唯一恢复真实规则,也不代表所有未知组合都可靠。
对 NGRA 的启示:新序列不是必然要求新增 relation;需要判断现有结构能否表达新的行为,以及扩展结构是否改善泛化而非只记住样例。
### 11.3 加权计算需要定义路径组合与路径汇总
一个一般的加权路径计算可以示意为:
```text
f(x) = ⊕_{π 为消费 x 的接受路径} [初始权重 ⊗ 路径各边权重 ⊗ 终止权重]
```
其中 ⊗ 表示同一路径中如何组合,⊕ 表示不同路径之间如何汇总。不同运算意味着不同语义:
| 组合与汇总 | 含义示例 |
|---|---|
| AND / OR | 是否存在接受路径 |
| 乘法 / 求和 | 汇总路径权重;满足概率约束时可解释为概率计算 |
| 乘法 / max | 最强路径,常见于非负权重下的最佳路径计算 |
| 加法 / max | 累积路径得分,再选择最佳路径 |
| min / max | 一种模糊隶属度组合方式 |
半环等代数条件使动态规划与路径汇总能够一致地组合,但任意设计的 relation/mixer 并不自动满足这些条件。数值在 [0,1] 内,也不自动是概率。
用户的“case 优先级加权”只是加权模型的一种用途;文献中的 weighted automaton 还包括路径得分、代价及概率。模糊匹配也不必只保留最高分路径。
### 11.4 实数加权自动机与谱学习
不含 ε 转移的实数加权自动机可写成:
```text
f(x₁…x_T) = αᵀ A_{x₁} … A_{x_T} ω
H[u,v] = f(uv) # Hankel 矩阵
```
在实数域的相应线性表示理论中,有限 Hankel 秩对应有限维加权自动机表示,最小表示维数由该秩刻画。谱学习通过有限子矩阵的估计和分解恢复模型,仍依赖数据与可识别性条件。
潜在维度不保证是可命名的语义关系,实数分解也不自动产生合法概率或隶属度。该理论不能直接移植到任意非线性、动态增删结构或任意 max/min 计算。
参考:[模糊正则到模糊自动机](https://arxiv.org/abs/1105.6190)、[实数加权自动机谱学习](https://cs.nyu.edu/~mohri/pub/swa.pdf)、[符号加权自动机查询学习](https://proceedings.mlr.press/v153/suzuki21a.html)。
## 12. Transformer 数学补充:为什么能输出上下文外的词
### 12.1 两次打分具有不同的候选范围
以列向量 h 表示最终位置状态,一个标准预测过程为:
```text
输入 token ID → 词嵌入与位置信息 → 多个 block → 最终状态 h
block 中:
Q = H W_Q,K = H W_K,V = H W_V
A = softmax(QKᵀ / √d_k + mask) # 沿允许的 key 位置归一化
O = A V # 混合上下文信息
随后还有输出投影、残差、归一化、前馈网络等;顺序取决于变体
词表输出:
s_i = w_iᵀ h + b_i # i 遍历整个词表
p_i = exp(s_i) / Σ_j exp(s_j)
```
前一个 softmax 回答“读哪些位置、各读多少”;后一个回答“输出哪个词表符号”。标准 V 的行数由被读取序列长度决定,不由词表大小决定。交叉注意力也可读取另一段序列,但仍不能把它默认等同于全词表。
### 12.2 本次讨论中的数值例子
忽略偏置,设 h=[2,1,-1],输出表有四行:
| token | 输出权重 w_i | 点积得分 |
|---|---|---:|
| 我 | [0,1,0] | 1 |
| 吃饭 | [1,0,0] | 2 |
| 灯塔 | [1,1,-1] | 4 |
| 苹果 | [-1,0,1] | -3 |
即使“灯塔”没有出现在输入里,它对应的输出参数行仍参与点积。此例仅说明计算过程,向量坐标不是人为规定的真实语义属性。
训练时,目标 token 为 y,交叉熵为:
```text
loss = -log p_y
∂loss/∂s_i = p_i - 1[i=y] # 标准全词表 softmax 下
```
因此目标输出与其他输出的得分共同参与训练,梯度也会通过 h 传回前面的网络。模型能输出未在当前上下文出现的词,来自这个训练过的映射,不需要先将目标词放进注意力 V。
### 12.3 条件、参数与随机采样
```text
Pθ(x₁…x_T) = ∏_t Pθ(x_t | x_{<t})
```
上下文提供条件,θ 保存训练形成的计算规律,解码策略从概率分布选择输出。随机种子只涉及随机选择,不是上下文本身;贪心选择可以不使用随机抽样。
网络可以组合训练所得规律生成未见过的整段文本,不代表该段文本作为完整记录储存在参数中,也不代表生成内容真实。
注意力行权重之和为 1,属于当前候选之间的相对分配:候选变化会影响权重。它不是可直接跨上下文比较的绝对事实置信度。强注意力也不自动证明某项输入对最终答案具有唯一因果作用。
### 12.4 参数、激活与复杂度
- 词表 N、上下文 L、隐藏维度 d 是不同轴,无须满足 L>N。
- 数学上的注意力得分矩阵为每头 L×L;高效实现不一定完整物化它。
- 稠密注意力的主要位置交互计算约为 O(L²d);投影、前馈网络及全词表输出另有成本。
- 给单个位置做标准全词表输出投影约为 O(Nd),不因输入里只有少数词而自动缩小。
- 增长的 KV 缓存等是运行时状态,不等于新增训练参数。
对 NGRA:输入候选路由与输出理念选择是两个问题。小型 mixer 产生 h 后仍可映射到大字典;若也要避免全字典输出计算,需增加输出候选检索/路由,而非要求字典都进入局部 V。
## 13. 数学与研究判断的边界
### 13.1 必须分别验证的性质
| 性质 | 问题 |
|---|---|
| 表达能力 | 是否存在能表达目标行为的参数/结构? |
| 可学习性 | 给定数据与反馈,能否找到这种结构? |
| 执行确定性 | 固定输入、状态及选择规则,是否输出一致? |
| 逻辑可靠性 | 结论是否由有效规则和适用前提支持? |
| 唯一解释 | 信息是否足以排除其他候选? |
| 工程效率 | 把路由、访存、同步算进去后是否实际更快? |
不能从“确定地取最大值”推出语义无歧义,也不能从“存在理论表示”推出可高效训练。减少活跃张量不自动减少全局参数,更不自动提高 GPU 吞吐。
### 13.2 关系强度不等于关系组合规则
“位于某城市”与“城市位于某国家”可在适当语义下组合;“喜欢”不能因两条边都强就自动传递。需要保留关系类型、方向、角色和条件。不同证据路径还可能重复使用同一事实,简单累加可能制造虚假支持。
同时,长距离不等于弱关系;当前低分不等于未来无用;稀疏不等于必然缺少长程能力。潜在难点是低显著度线索被过早删除,或语义压缩丢失后来需要的区别。
### 13.3 联合学习与预测淘汰的候选形式
讨论中的结构/参数学习可概括为研究目标:
```text
min_{G,θ} Σ_(x,y) loss(f_{G,θ}(x),y) + λ C(G)
```
G 为关系结构,θ 为数值参数,C(G) 惩罚结构复杂度。这只是问题定义,不提供自动增删关系或反向传播穿过离散操作的算法。
预测淘汰可用启发式价值表达:
```text
保留价值 ≈ 未来使用概率 × 遗忘损失 − 保留成本
```
实际还应计入召回成功率、召回成本和时间范围;这些量需要学习或明确估计,不能把当前激活值直接代入所有项。
### 13.4 关于“封闭”的最终澄清
固定有限词表 D 的长度至多 L 的序列集合有限;D* 在非空字表、长度无上限时无限。有限维实向量空间在数学上无限,机器有限精度不使实际学习与枚举问题自动变简单。
输入和输出共享词表,只说明可在相同符号系统中表达,不证明推理正确,也不构成预测未出现符号的充分解释。真正需要明确的是输入状态如何经可学习函数映射为输出候选及其得分。
+19 -2
View File
@@ -7,7 +7,7 @@
// //
// Permission is hereby granted, free of charge, to any person // Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation // obtaining a copy of this software and associated documentation
// files (the ¡°Software¡±), to deal in the Software without restriction, // files (the ��Software��), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge, // including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, // publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so, // and to permit persons to whom the Software is furnished to do so,
@@ -17,7 +17,7 @@
// this permission notice shall be included in all copies // this permission notice shall be included in all copies
// or substantial portions of the Software. // or substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED ¡°AS IS¡±, // THE SOFTWARE IS PROVIDED ��AS IS��,
// WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -32,6 +32,7 @@ using StgSharp;
using StgSharp.Collections; using StgSharp.Collections;
using StgSharp.Script; using StgSharp.Script;
using StgSharp.Script.Express; using StgSharp.Script.Express;
using Nitload.Common.Collections;
//using StgSharp.Model; //using StgSharp.Model;
@@ -46,6 +47,22 @@ namespace STgSharpTest
{ {
string testExpPath = @"D:\Programmer\stepcode\data\ap203\ap203.exp"; string testExpPath = @"D:\Programmer\stepcode\data\ap203\ap203.exp";
[TestMethod]
public void SwissTableOperationsAreNotImplemented()
{
using SwissTable table = SwissTable.Create();
nuint key = 17;
Assert.ThrowsException<NotImplementedException>(
() => SwissTable.Reader.TryGet(table, key, out _));
Assert.ThrowsException<NotImplementedException>(
() => SwissTable.Writer.TryAdd(table, key, 100));
Assert.ThrowsException<NotImplementedException>(
() => SwissTable.Writer.TrySet(table, key, 100));
Assert.ThrowsException<NotImplementedException>(
() => SwissTable.Writer.TryRemove(table, key, out _));
}
[TestMethod] [TestMethod]
public void RegexTest() public void RegexTest()
{ {
+2 -2
View File
@@ -24,7 +24,7 @@ namespace Nitload
for (nuint i = 0; i < 5000; i++) for (nuint i = 0; i < 5000; i++)
{ {
Console.Write($"Set {i}: "); Console.Write($"Set {i}: ");
_ = swiss.TryAddOrSet(i, i * 2, out _); _ = SwissTable.Writer.TrySet(swiss, i, i * 2);
Console.WriteLine($"Set a kvp of ({i},{i * 2})"); Console.WriteLine($"Set a kvp of ({i},{i * 2})");
} }
Random rand = new Random(); Random rand = new Random();
@@ -32,7 +32,7 @@ namespace Nitload
{ {
Console.Write($"Get {i}: "); Console.Write($"Get {i}: ");
nuint key = (nuint)rand.Next(4000, 5000); nuint key = (nuint)rand.Next(4000, 5000);
if (swiss.TryGet(key, out nuint value)) if (SwissTable.Reader.TryGet(swiss, key, out nuint value))
{ {
Console.WriteLine($"Success in get a kvp of ({key},{value})"); Console.WriteLine($"Success in get a kvp of ({key},{value})");
} else } else