Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions TextRecast.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@
<Project Path="tests/TextRecast.Core.Tests/TextRecast.Core.Tests.csproj" />
<Project Path="tests/TextRecast.Infrastructure.Tests/TextRecast.Infrastructure.Tests.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/TextRecast.ModelBenchmarks/TextRecast.ModelBenchmarks.csproj" />
</Folder>
</Solution>
94 changes: 94 additions & 0 deletions src/TextRecast.Infrastructure/Hardware/HardwareInspector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.IO;
using System.Runtime.InteropServices;

namespace TextRecast.Infrastructure.Hardware;

public sealed class HardwareInspector
{
private readonly IHardwareProbe _probe;

public HardwareInspector()
: this(new WindowsHardwareProbe())
{
}

internal HardwareInspector(IHardwareProbe probe)
{
_probe = probe;
}

public HardwareProfile Inspect(string modelStoragePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(modelStoragePath);

try
{
var memory = _probe.ReadPhysicalMemory();
var logicalProcessors = _probe.ReadLogicalProcessorCount();
var architecture = _probe.ReadProcessArchitecture();
var supportsAvx2 = _probe.ReadAvx2Support();
var storage = _probe.ReadStorage(modelStoragePath);

if (memory.TotalBytes <= 0)
{
throw new HardwareInspectionException(
"Windows did not report the computer's total physical memory.");
}

if (memory.AvailableBytes < 0 || memory.AvailableBytes > memory.TotalBytes)
{
throw new HardwareInspectionException(
"Windows reported an invalid available physical-memory value.");
}

if (logicalProcessors <= 0)
{
throw new HardwareInspectionException(
"The logical processor count is unavailable.");
}

if (storage.AvailableBytes < 0 || string.IsNullOrWhiteSpace(storage.RootPath))
{
throw new HardwareInspectionException(
"Available storage for the model directory is unavailable.");
}

return new HardwareProfile(
memory.TotalBytes,
memory.AvailableBytes,
logicalProcessors,
architecture,
supportsAvx2,
storage.AvailableBytes,
storage.RootPath);
}
catch (HardwareInspectionException)
{
throw;
}
catch (Exception exception) when (
exception is IOException or UnauthorizedAccessException or System.ComponentModel.Win32Exception)
{
throw new HardwareInspectionException(
"TextRecast could not inspect this computer's hardware capabilities.",
exception);
}
}
}

internal interface IHardwareProbe
{
PhysicalMemorySnapshot ReadPhysicalMemory();

int ReadLogicalProcessorCount();

Architecture ReadProcessArchitecture();

bool ReadAvx2Support();

StorageSnapshot ReadStorage(string modelStoragePath);
}

internal readonly record struct PhysicalMemorySnapshot(long TotalBytes, long AvailableBytes);

internal readonly record struct StorageSnapshot(long AvailableBytes, string RootPath);
25 changes: 25 additions & 0 deletions src/TextRecast.Infrastructure/Hardware/HardwareProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Runtime.InteropServices;

namespace TextRecast.Infrastructure.Hardware;

public sealed record HardwareProfile(
long TotalPhysicalMemoryBytes,
long AvailablePhysicalMemoryBytes,
int LogicalProcessorCount,
Architecture ProcessArchitecture,
bool SupportsAvx2,
long AvailableModelStorageBytes,
string ModelStorageRoot);

public sealed class HardwareInspectionException : Exception
{
public HardwareInspectionException(string message)
: base(message)
{
}

public HardwareInspectionException(string message, Exception innerException)
: base(message, innerException)
{
}
}
73 changes: 73 additions & 0 deletions src/TextRecast.Infrastructure/Hardware/WindowsHardwareProbe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;

namespace TextRecast.Infrastructure.Hardware;

internal sealed class WindowsHardwareProbe : IHardwareProbe
{
public PhysicalMemorySnapshot ReadPhysicalMemory()
{
var status = new MemoryStatus
{
Length = checked((uint)Marshal.SizeOf<MemoryStatus>())
};
if (!GlobalMemoryStatusEx(ref status))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}

return new PhysicalMemorySnapshot(
checked((long)status.TotalPhysical),
checked((long)status.AvailablePhysical));
}

public int ReadLogicalProcessorCount()
{
return Environment.ProcessorCount;
}

public Architecture ReadProcessArchitecture()
{
return RuntimeInformation.ProcessArchitecture;
}

public bool ReadAvx2Support()
{
return Avx2.IsSupported;
}

public StorageSnapshot ReadStorage(string modelStoragePath)
{
var root = ResolveStorageRoot(modelStoragePath);
var drive = new DriveInfo(root);
return new StorageSnapshot(drive.AvailableFreeSpace, drive.RootDirectory.FullName);
}

internal static string ResolveStorageRoot(string modelStoragePath)
{
var fullPath = Path.GetFullPath(modelStoragePath);
var root = Path.GetPathRoot(fullPath);
return string.IsNullOrWhiteSpace(root)
? throw new IOException("The model-storage drive could not be determined.")
: root;
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalMemoryStatusEx(ref MemoryStatus buffer);

[StructLayout(LayoutKind.Sequential)]
private struct MemoryStatus
{
public uint Length;
public uint MemoryLoad;
public ulong TotalPhysical;
public ulong AvailablePhysical;
public ulong TotalPageFile;
public ulong AvailablePageFile;
public ulong TotalVirtual;
public ulong AvailableVirtual;
public ulong AvailableExtendedVirtual;
}
}
19 changes: 19 additions & 0 deletions src/TextRecast.Infrastructure/SLM/ISlmModelAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using LLama.Sampling;
using TextRecast.Core.Formatting;

namespace TextRecast.Infrastructure.SLM;

public interface ISlmModelAdapter
{
string Id { get; }

IReadOnlyList<string> StopSequences { get; }

string BuildPrompt(FormatTextRequest request);

ISamplingPipeline CreateSamplingPipeline();

int GetExpectedOutputWordCount(FormatTextRequest request);

string CleanOutput(string output);
}
8 changes: 0 additions & 8 deletions src/TextRecast.Infrastructure/SLM/ISlmPromptBuilder.cs

This file was deleted.

33 changes: 9 additions & 24 deletions src/TextRecast.Infrastructure/SLM/LocalSlmTextFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using LLama;
using LLama.Common;
using LLama.Native;
using LLama.Sampling;
using TextRecast.Core.Abstractions;
using TextRecast.Core.Formatting;

Expand All @@ -17,22 +16,22 @@ public sealed class LocalSlmTextFormatter : ITextFormatter
private const int MaxChunkCharacters = 450;
private const int MinimumOutputTokens = 64;
private readonly SlmModelOptions _options;
private readonly ISlmPromptBuilder _promptBuilder;
private readonly ISlmModelAdapter _adapter;
private readonly SemaphoreSlim _inferenceGate = new(1, 1);
private readonly CancellationTokenSource _shutdownCancellation = new();
private LLamaWeights? _weights;
private StatelessExecutor? _executor;
private int _disposeState;

public LocalSlmTextFormatter(SlmModelOptions options)
: this(options, new ChatMlPromptBuilder())
: this(options, SlmModelAdapterRegistry.Default.Resolve(options.Profile))
{
}

public LocalSlmTextFormatter(SlmModelOptions options, ISlmPromptBuilder promptBuilder)
internal LocalSlmTextFormatter(SlmModelOptions options, ISlmModelAdapter adapter)
{
_options = options;
_promptBuilder = promptBuilder;
_adapter = adapter;
}

public async Task<string> FormatAsync(FormatTextRequest request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -125,7 +124,7 @@ private async Task<string> FormatSingleAsync(
FormatTextRequest request,
CancellationToken cancellationToken)
{
var prompt = _promptBuilder.Build(request);
var prompt = _adapter.BuildPrompt(request);
return await Task.Run(
() => InferAsync(request, prompt, cancellationToken),
cancellationToken);
Expand Down Expand Up @@ -162,8 +161,8 @@ private async Task<string> InferAsync(
var inferenceParams = new InferenceParams
{
MaxTokens = GetOutputTokenBudget(request, prompt),
AntiPrompts = ["<|im_end|>", "<|im_start|>"],
SamplingPipeline = new GreedySamplingPipeline()
AntiPrompts = [.. _adapter.StopSequences],
SamplingPipeline = _adapter.CreateSamplingPipeline()
};

var output = new StringBuilder();
Expand All @@ -175,7 +174,7 @@ private async Task<string> InferAsync(
output.Append(token);
}

return RemoveProtocolMarkers(output.ToString());
return _adapter.CleanOutput(output.ToString());
}

private int GetOutputTokenBudget(FormatTextRequest request, string prompt)
Expand All @@ -188,14 +187,7 @@ private int GetOutputTokenBudget(FormatTextRequest request, string prompt)
"This selection exceeds the local model's context capacity. Try a smaller section.");
}

var inputWords = ChatMlPromptBuilder.CountWords(request.Text);
var expectedOutputWords = request.Operation switch
{
FormatOperation.Shorten => ChatMlPromptBuilder.GetShorterWordTarget(inputWords),
FormatOperation.Lengthen => ChatMlPromptBuilder.GetLongerWordTarget(inputWords),
FormatOperation.Summarize => ChatMlPromptBuilder.GetSummaryWordTarget(inputWords),
_ => inputWords
};
var expectedOutputWords = _adapter.GetExpectedOutputWordCount(request);
var desiredTokens = Math.Clamp(
(int)Math.Ceiling(expectedOutputWords * 1.9) + 48,
MinimumOutputTokens,
Expand Down Expand Up @@ -271,11 +263,4 @@ private async Task VerifyModelAsync(CancellationToken cancellationToken)
}
}

private static string RemoveProtocolMarkers(string output)
{
return output
.Replace("<|im_end|>", string.Empty, StringComparison.Ordinal)
.Replace("<|im_start|>", string.Empty, StringComparison.Ordinal)
.Trim();
}
}
Loading
Loading