Skip to content

Commit 565c4ef

Browse files
franklinicclaude
andcommitted
merge: resolve master conflict on Directory.Packages.props (OneDNN 0.46.0)
Only conflict — AiDotNet.Native.OneDNN version: - HEAD (this branch): 0.46.0 (ecosystem-matching with Tensors 0.46.0 bump) - origin/master: 0.38.0 (stale) Kept 0.46.0 since this PR's whole purpose is the Tensors-parity bump, and native lib versions stay in lockstep with the Tensors major/minor they back. NuGet confirms 0.46.0 exists (current is 0.48.0; not bumping further to keep scope tight). Build: net10.0 + net471 both clean post-merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents d59e029 + 825519c commit 565c4ef

12 files changed

Lines changed: 650 additions & 26 deletions

File tree

src/ComputerVision/Weights/WeightDownloader.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net.Http;
2+
using AiDotNet.Data;
23

34
namespace AiDotNet.ComputerVision.Weights;
45

@@ -108,8 +109,10 @@ public async Task DownloadAsync(
108109
}
109110
}
110111

111-
// Move completed download to final location
112-
File.Move(tempPath, localPath);
112+
// Move completed download to final location. Uses RobustFileOps
113+
// so a transient Windows Defender / indexer lock doesn't abort
114+
// the download (the finally block would wipe the temp file).
115+
await RobustFileOps.MoveWithRetryAsync(tempPath, localPath, cancellationToken);
113116
}
114117
finally
115118
{

src/Data/DatasetDownloader.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,35 @@ public static async Task<bool> DownloadFileAsync(
7373
string tempPath = destinationPath + ".tmp";
7474
try
7575
{
76-
using var response = await SharedHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
77-
response.EnsureSuccessStatusCode();
76+
using (var response = await SharedHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
77+
{
78+
response.EnsureSuccessStatusCode();
7879

79-
using var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None);
80+
// Close the file stream explicitly (via nested using) before
81+
// File.Move runs. The nested scope guarantees the handle is
82+
// released; otherwise the outer `using` would dispose *after*
83+
// the move attempt.
84+
using var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None);
8085
#if NET6_0_OR_GREATER
81-
await response.Content.CopyToAsync(fileStream, cancellationToken);
86+
await response.Content.CopyToAsync(fileStream, cancellationToken);
87+
await fileStream.FlushAsync(cancellationToken);
8288
#else
83-
await response.Content.CopyToAsync(fileStream);
89+
await response.Content.CopyToAsync(fileStream);
90+
fileStream.Flush();
8491
#endif
92+
}
8593

86-
// Move temp file to final location
94+
// Move temp file to final location.
8795
if (File.Exists(destinationPath))
8896
{
8997
File.Delete(destinationPath);
9098
}
9199

92-
File.Move(tempPath, destinationPath);
100+
// Retry the rename. On Windows the freshly-written file can be
101+
// briefly locked by antivirus (Defender) or the search indexer,
102+
// which makes File.Move fail with a sharing violation even after
103+
// the writer's handle is closed. Short backoff tolerates that.
104+
await RobustFileOps.MoveWithRetryAsync(tempPath, destinationPath, cancellationToken);
93105
return true;
94106
}
95107
finally

src/Data/RobustFileOps.cs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
namespace AiDotNet.Data;
2+
3+
/// <summary>
4+
/// Filesystem helpers that tolerate transient locks — particularly the
5+
/// "sharing violation" window that Windows Defender or the search indexer
6+
/// can hold on a freshly written file for a few hundred milliseconds after
7+
/// the writer's handle is released.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// Every download path in this library writes to a temp file and then
12+
/// renames to the final location. On Windows, an antivirus scanner or the
13+
/// search indexer can hold a secondary handle on the temp file briefly
14+
/// after the writer closes it — long enough that the immediate
15+
/// <c>File.Move</c> fails with <see cref="IOException"/>. Without retry
16+
/// logic, a single transient lock aborts the entire download; the caller
17+
/// then typically wipes the temp file in a <c>finally</c> block and must
18+
/// re-download from scratch, which will race again the next time.
19+
/// </para>
20+
/// <para>
21+
/// <see cref="MoveWithRetryAsync"/> retries on <see cref="IOException"/>
22+
/// and <see cref="UnauthorizedAccessException"/> with linear backoff. The
23+
/// final attempt still propagates the real exception so callers see a
24+
/// clear failure rather than a silent no-op.
25+
/// </para>
26+
/// </remarks>
27+
internal static class RobustFileOps
28+
{
29+
/// <summary>
30+
/// Moves <paramref name="sourcePath"/> to <paramref name="destinationPath"/>,
31+
/// tolerating transient Windows-style sharing violations.
32+
/// </summary>
33+
/// <param name="sourcePath">The source file path (typically a temp file).</param>
34+
/// <param name="destinationPath">The destination path (final location).</param>
35+
/// <param name="cancellationToken">Cancellation token.</param>
36+
/// <param name="maxAttempts">Maximum total attempts (default 5).</param>
37+
/// <param name="initialDelayMs">Initial delay between retries; each subsequent
38+
/// retry waits <c>attempt * initialDelayMs</c> milliseconds (default 200 ms,
39+
/// so default schedule is 200 / 400 / 600 / 800 ms).</param>
40+
/// <exception cref="IOException">Final attempt failed with an IO error.</exception>
41+
/// <exception cref="UnauthorizedAccessException">Final attempt failed with an access error.</exception>
42+
/// <remarks>
43+
/// <para>
44+
/// The retry tolerates <see cref="IOException"/> (e.g. sharing violation on
45+
/// Windows) and <see cref="UnauthorizedAccessException"/> (e.g. ACL
46+
/// contention mid-move). Other exceptions are surfaced immediately.
47+
/// </para>
48+
/// </remarks>
49+
internal static async Task MoveWithRetryAsync(
50+
string sourcePath,
51+
string destinationPath,
52+
CancellationToken cancellationToken,
53+
int maxAttempts = 5,
54+
int initialDelayMs = 200)
55+
{
56+
ValidateRetryArguments(maxAttempts, initialDelayMs);
57+
58+
for (int attempt = 1; attempt <= maxAttempts; attempt++)
59+
{
60+
try
61+
{
62+
File.Move(sourcePath, destinationPath);
63+
return;
64+
}
65+
// On the final attempt the `when` clause is false, so the
66+
// catch is skipped and the original exception propagates to
67+
// the caller — exactly the intended "clear failure" behavior.
68+
catch (IOException) when (attempt < maxAttempts)
69+
{
70+
await Task.Delay(TimeSpan.FromMilliseconds(initialDelayMs * attempt), cancellationToken);
71+
}
72+
catch (UnauthorizedAccessException) when (attempt < maxAttempts)
73+
{
74+
await Task.Delay(TimeSpan.FromMilliseconds(initialDelayMs * attempt), cancellationToken);
75+
}
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Synchronous sibling of <see cref="MoveWithRetryAsync"/>. For call sites
81+
/// that can't go async (e.g. atomic index flushes from a sync save path).
82+
/// Uses <see cref="Thread.Sleep"/> between attempts; prefer the async
83+
/// variant when an awaitable context is available.
84+
/// </summary>
85+
internal static void MoveWithRetry(
86+
string sourcePath,
87+
string destinationPath,
88+
int maxAttempts = 5,
89+
int initialDelayMs = 200)
90+
{
91+
ValidateRetryArguments(maxAttempts, initialDelayMs);
92+
93+
for (int attempt = 1; attempt <= maxAttempts; attempt++)
94+
{
95+
try
96+
{
97+
File.Move(sourcePath, destinationPath);
98+
return;
99+
}
100+
// Final attempt falls through `when (attempt < maxAttempts)`,
101+
// so the underlying exception naturally propagates.
102+
catch (IOException) when (attempt < maxAttempts)
103+
{
104+
Thread.Sleep(initialDelayMs * attempt);
105+
}
106+
catch (UnauthorizedAccessException) when (attempt < maxAttempts)
107+
{
108+
Thread.Sleep(initialDelayMs * attempt);
109+
}
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Atomically replaces <paramref name="destinationPath"/> with
115+
/// <paramref name="sourcePath"/>, optionally keeping a backup at
116+
/// <paramref name="destinationBackupPath"/>. Retries on transient
117+
/// <see cref="IOException"/> / <see cref="UnauthorizedAccessException"/>
118+
/// with linear backoff, mirroring <see cref="MoveWithRetry"/>.
119+
/// Synchronous; used by atomic index / checkpoint flush paths that are
120+
/// not async.
121+
/// </summary>
122+
/// <remarks>
123+
/// On Windows <see cref="File.Replace(string, string, string)"/> is the
124+
/// in-place atomic rename, but it can still fail with a sharing
125+
/// violation if Windows Defender or the search indexer is mid-scan of
126+
/// either file.
127+
/// </remarks>
128+
internal static void ReplaceWithRetry(
129+
string sourcePath,
130+
string destinationPath,
131+
string? destinationBackupPath,
132+
int maxAttempts = 5,
133+
int initialDelayMs = 200)
134+
{
135+
ValidateRetryArguments(maxAttempts, initialDelayMs);
136+
137+
for (int attempt = 1; attempt <= maxAttempts; attempt++)
138+
{
139+
try
140+
{
141+
File.Replace(sourcePath, destinationPath, destinationBackupPath);
142+
return;
143+
}
144+
// Final attempt: the `when` gate is false so the exception
145+
// propagates directly to the caller.
146+
catch (IOException) when (attempt < maxAttempts)
147+
{
148+
Thread.Sleep(initialDelayMs * attempt);
149+
}
150+
catch (UnauthorizedAccessException) when (attempt < maxAttempts)
151+
{
152+
Thread.Sleep(initialDelayMs * attempt);
153+
}
154+
}
155+
}
156+
157+
/// <summary>
158+
/// Validates retry parameters. Rejects silent-success configurations —
159+
/// <c>maxAttempts &lt; 1</c> would exit the retry loop without ever
160+
/// touching the filesystem and without throwing, which is a confusing
161+
/// failure mode for any caller that forwards user-supplied config.
162+
/// </summary>
163+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="maxAttempts"/>
164+
/// is less than 1, or <paramref name="initialDelayMs"/> is negative.</exception>
165+
private static void ValidateRetryArguments(int maxAttempts, int initialDelayMs)
166+
{
167+
if (maxAttempts < 1)
168+
{
169+
throw new ArgumentOutOfRangeException(
170+
nameof(maxAttempts),
171+
maxAttempts,
172+
"maxAttempts must be at least 1.");
173+
}
174+
175+
if (initialDelayMs < 0)
176+
{
177+
throw new ArgumentOutOfRangeException(
178+
nameof(initialDelayMs),
179+
initialDelayMs,
180+
"initialDelayMs must be non-negative.");
181+
}
182+
}
183+
}

src/Data/TimeSeries/M4DatasetLoader.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,12 +366,16 @@ private async Task DownloadFileAtomicAsync(string url, string targetPath, string
366366
// Write to temp file first
367367
await FilePolyfill.WriteAllTextAsync(tempPath, content, cancellationToken);
368368

369-
// Atomically move to final location
369+
// Atomically move to final location. Uses RobustFileOps to
370+
// tolerate a transient Windows Defender / indexer lock on the
371+
// freshly written temp file (File.Move would otherwise fail
372+
// with a sharing violation and the finally block would wipe
373+
// the content).
370374
if (File.Exists(targetPath))
371375
{
372376
File.Delete(targetPath);
373377
}
374-
File.Move(tempPath, targetPath);
378+
await RobustFileOps.MoveWithRetryAsync(tempPath, targetPath, cancellationToken);
375379
}
376380
finally
377381
{

src/Data/Vision/Benchmarks/Cifar100DataLoader.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ protected override async Task LoadDataCoreAsync(CancellationToken cancellationTo
9999
if (!nchw)
100100
sampleTensor = AiDotNetEngine.Current.TensorPermute(sampleTensor, [1, 2, 0]);
101101

102-
sampleTensor.AsSpan().CopyTo(featuresData.AsSpan(featureOffset, ppi));
102+
// Tensor<T>.CopyTo handles the strided post-permute case in a
103+
// single pass; see Cifar10DataLoader for the rationale.
104+
sampleTensor.CopyTo(featuresData.AsSpan(featureOffset, ppi));
103105

104106
if (label >= 0 && label < _numClasses)
105107
labelsData[i * _numClasses + label] = NumOps.One;

src/Data/Vision/Benchmarks/Cifar10DataLoader.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ protected override async Task LoadDataCoreAsync(CancellationToken cancellationTo
112112
if (!nchw)
113113
sampleTensor = AiDotNetEngine.Current.TensorPermute(sampleTensor, [1, 2, 0]);
114114

115-
sampleTensor.AsSpan().CopyTo(featuresData.AsSpan(featureOffset, pixelsPerImage));
115+
// Tensor<T>.CopyTo handles both contiguous and strided (post-permute)
116+
// layouts in a single pass. Using AsSpan() here would throw
117+
// "Cannot get a contiguous span from a non-contiguous tensor view"
118+
// on the NHWC default path.
119+
sampleTensor.CopyTo(featuresData.AsSpan(featureOffset, pixelsPerImage));
116120

117121
if (label >= 0 && label < 10)
118122
labelsData[i * 10 + label] = NumOps.One;

src/Data/Vision/Benchmarks/EuroSatDataLoader.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,13 @@ protected override async Task LoadDataCoreAsync(CancellationToken cancellationTo
125125
bool nchw = _options.Layout == ImageTensorLayout.NCHW;
126126
if (nchw)
127127
{
128-
// VisionLoaderHelper returns HWC [H,W,3]; permute to CHW [3,H,W]
128+
// VisionLoaderHelper returns HWC [H,W,3]; permute to CHW [3,H,W].
129+
// Tensor<T>.CopyTo handles the strided post-permute view in a
130+
// single pass without allocating an intermediate contiguous tensor.
129131
var hwcTensor = new Tensor<T>([ImageSize, ImageSize, 3]);
130132
pixels.AsSpan(0, available).CopyTo(hwcTensor.AsWritableSpan());
131133
var chwTensor = AiDotNetEngine.Current.TensorPermute(hwcTensor, [2, 0, 1]);
132-
chwTensor.AsSpan().CopyTo(featuresData.AsSpan(featureOffset, pixelsPerImage));
134+
chwTensor.CopyTo(featuresData.AsSpan(featureOffset, pixelsPerImage));
133135
}
134136
else
135137
{

src/ModelLoading/HuggingFaceModelLoader.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http.Headers;
33
using System.Security.Cryptography;
44
using System.Text;
5+
using AiDotNet.Data;
56
using AiDotNet.Interfaces;
67
using Newtonsoft.Json.Linq;
78

@@ -259,12 +260,15 @@ public async Task<string> DownloadFileAsync(
259260
}
260261
}
261262

262-
// Move temp to final location (overwrite parameter not available in .NET Framework 4.7.1)
263+
// Move temp to final location (overwrite parameter not available in .NET Framework 4.7.1).
264+
// RobustFileOps tolerates a transient antivirus / indexer lock on
265+
// the freshly written temp file that would otherwise fail with a
266+
// sharing violation.
263267
if (File.Exists(localPath))
264268
{
265269
File.Delete(localPath);
266270
}
267-
File.Move(tempPath, localPath);
271+
await RobustFileOps.MoveWithRetryAsync(tempPath, localPath, cancellationToken);
268272
}
269273
catch (HttpRequestException ex)
270274
{

src/Onnx/OnnxModelDownloader.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http.Headers;
33
using System.Security.Cryptography;
44
using System.Text.Json;
5+
using AiDotNet.Data;
56
using AiDotNet.Interfaces;
67
using AiDotNet.Validation;
78

@@ -306,15 +307,18 @@ private async Task DownloadFileAsync(
306307
}
307308
}
308309

309-
// Move completed download to final location
310+
// Move completed download to final location. RobustFileOps
311+
// tolerates a transient antivirus / indexer lock on the freshly
312+
// written temp file that would otherwise fail with a sharing
313+
// violation and lose the download.
310314
fileStream.Close();
311315

312316
if (File.Exists(localPath))
313317
{
314318
File.Delete(localPath);
315319
}
316320

317-
File.Move(tempPath, localPath);
321+
await RobustFileOps.MoveWithRetryAsync(tempPath, localPath, cancellationToken);
318322
progress?.Report(1.0);
319323
}
320324

0 commit comments

Comments
 (0)