@mlx-node/trl 0.0.12 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve as resolvePath } from 'node:path';
3
+
4
+ import { parse as parseToml } from '@std/toml';
5
+ import { camelCase } from 'change-case';
6
+
7
+ export class SFTConfigError extends Error {
8
+ constructor(message: string) {
9
+ super(message);
10
+ this.name = 'SFTConfigError';
11
+ }
12
+ }
13
+
14
+ export interface SFTTrainerConfig {
15
+ // Model
16
+ modelName: string;
17
+ outputDir: string;
18
+ runName: string;
19
+
20
+ // Training hyperparameters
21
+ learningRate: number;
22
+ batchSize: number;
23
+ gradientAccumulationSteps: number;
24
+ numEpochs: number;
25
+ maxTrainSamples: number;
26
+ maxGradNorm: number;
27
+ weightDecay: number;
28
+
29
+ // SFT-specific
30
+ maxSeqLength: number;
31
+ completionOnly: boolean;
32
+ labelSmoothing: number;
33
+
34
+ // Logging & checkpointing
35
+ loggingSteps: number;
36
+ saveSteps: number;
37
+ maxCheckpoints: number;
38
+ logJsonl: boolean;
39
+ tuiMode: boolean;
40
+
41
+ // Memory optimization
42
+ gradientCheckpointing: boolean;
43
+
44
+ // Misc
45
+ seed: number;
46
+ resumeFromCheckpoint: string;
47
+ }
48
+
49
+ const DEFAULT_SFT_CONFIG: SFTTrainerConfig = Object.freeze({
50
+ modelName: 'Qwen/Qwen3-0.6B',
51
+ outputDir: 'outputs/sft',
52
+ runName: 'sft-run',
53
+
54
+ learningRate: 2e-5,
55
+ batchSize: 4,
56
+ gradientAccumulationSteps: 1,
57
+ numEpochs: 3,
58
+ maxTrainSamples: 0,
59
+ maxGradNorm: 1.0,
60
+ weightDecay: 0.01,
61
+
62
+ maxSeqLength: 2048,
63
+ completionOnly: false, // Changed to false for TRL parity
64
+ labelSmoothing: 0.0,
65
+
66
+ loggingSteps: 10,
67
+ saveSteps: 100,
68
+ maxCheckpoints: 3,
69
+ logJsonl: true,
70
+ tuiMode: false,
71
+
72
+ gradientCheckpointing: true,
73
+
74
+ seed: 42,
75
+ resumeFromCheckpoint: '',
76
+ });
77
+
78
+ type SFTConfigKey = keyof SFTTrainerConfig;
79
+
80
+ const SFT_CONFIG_VALUE_TYPES: Record<SFTConfigKey, 'number' | 'boolean' | 'string'> = {
81
+ modelName: 'string',
82
+ outputDir: 'string',
83
+ runName: 'string',
84
+ learningRate: 'number',
85
+ batchSize: 'number',
86
+ gradientAccumulationSteps: 'number',
87
+ numEpochs: 'number',
88
+ maxTrainSamples: 'number',
89
+ maxGradNorm: 'number',
90
+ weightDecay: 'number',
91
+ maxSeqLength: 'number',
92
+ completionOnly: 'boolean',
93
+ labelSmoothing: 'number',
94
+ loggingSteps: 'number',
95
+ saveSteps: 'number',
96
+ maxCheckpoints: 'number',
97
+ logJsonl: 'boolean',
98
+ tuiMode: 'boolean',
99
+ seed: 'number',
100
+ gradientCheckpointing: 'boolean',
101
+ resumeFromCheckpoint: 'string',
102
+ };
103
+
104
+ const SFT_INTEGER_KEYS: ReadonlySet<SFTConfigKey> = new Set([
105
+ 'batchSize',
106
+ 'gradientAccumulationSteps',
107
+ 'numEpochs',
108
+ 'maxTrainSamples',
109
+ 'maxSeqLength',
110
+ 'loggingSteps',
111
+ 'saveSteps',
112
+ 'maxCheckpoints',
113
+ 'seed',
114
+ ]);
115
+
116
+ function cloneDefaults(): SFTTrainerConfig {
117
+ return { ...DEFAULT_SFT_CONFIG };
118
+ }
119
+
120
+ function isConfigKey(value: string): value is SFTConfigKey {
121
+ return Object.prototype.hasOwnProperty.call(SFT_CONFIG_VALUE_TYPES, value);
122
+ }
123
+
124
+ function coerceBoolean(value: unknown, key: SFTConfigKey): boolean {
125
+ if (typeof value === 'boolean') return value;
126
+ if (typeof value === 'string') {
127
+ const normalized = value.trim().toLowerCase();
128
+ if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
129
+ if (['false', '0', 'no', 'off'].includes(normalized)) return false;
130
+ }
131
+ throw new SFTConfigError(`Invalid boolean for ${key}: ${String(value)}`);
132
+ }
133
+
134
+ function coerceNumber(value: unknown, key: SFTConfigKey): number {
135
+ if (typeof value === 'string' && value.trim() === '') {
136
+ throw new SFTConfigError(`Invalid number for ${key}: empty string`);
137
+ }
138
+ const parsed = typeof value === 'number' ? value : Number(value);
139
+ if (!Number.isFinite(parsed)) {
140
+ throw new SFTConfigError(`Invalid number for ${key}: ${String(value)}`);
141
+ }
142
+ if (SFT_INTEGER_KEYS.has(key) && !Number.isInteger(parsed)) {
143
+ throw new SFTConfigError(`Expected integer for ${key}, received ${parsed}`);
144
+ }
145
+ return parsed;
146
+ }
147
+
148
+ function coerceString(value: unknown, key: SFTConfigKey): string {
149
+ if (typeof value === 'string') return value;
150
+ throw new SFTConfigError(`Invalid string for ${key}: ${String(value)}`);
151
+ }
152
+
153
+ function coerceValue(key: SFTConfigKey, value: unknown): SFTTrainerConfig[SFTConfigKey] {
154
+ const expected = SFT_CONFIG_VALUE_TYPES[key];
155
+ if (expected === 'boolean') {
156
+ return coerceBoolean(value, key) as SFTTrainerConfig[SFTConfigKey];
157
+ }
158
+ if (expected === 'number') {
159
+ return coerceNumber(value, key) as SFTTrainerConfig[SFTConfigKey];
160
+ }
161
+ return coerceString(value, key) as SFTTrainerConfig[SFTConfigKey];
162
+ }
163
+
164
+ function setConfigValue<T extends Partial<SFTTrainerConfig>>(
165
+ config: T,
166
+ key: SFTConfigKey,
167
+ value: SFTTrainerConfig[SFTConfigKey],
168
+ ): void {
169
+ (config as Record<SFTConfigKey, SFTTrainerConfig[SFTConfigKey]>)[key] = value;
170
+ }
171
+
172
+ function normalizeTomlRecord(record: Record<string, unknown>): Partial<SFTTrainerConfig> {
173
+ const normalized: Partial<SFTTrainerConfig> = {};
174
+ for (const [rawKey, rawValue] of Object.entries(record)) {
175
+ // TOML uses snake_case, config uses camelCase
176
+ const key = camelCase(rawKey);
177
+ if (!isConfigKey(key)) {
178
+ continue;
179
+ }
180
+ setConfigValue(normalized, key, coerceValue(key, rawValue));
181
+ }
182
+ return normalized;
183
+ }
184
+
185
+ export function getDefaultSFTConfig(): SFTTrainerConfig {
186
+ return cloneDefaults();
187
+ }
188
+
189
+ export function mergeSFTConfig(base: SFTTrainerConfig, update: Partial<SFTTrainerConfig>): SFTTrainerConfig {
190
+ if (!update) {
191
+ return { ...base };
192
+ }
193
+ const result: SFTTrainerConfig = { ...base };
194
+ for (const [key, value] of Object.entries(update) as [SFTConfigKey, SFTTrainerConfig[SFTConfigKey]][]) {
195
+ if (value === undefined) continue;
196
+ if (!isConfigKey(key)) {
197
+ throw new SFTConfigError(`Unknown configuration key: ${key as string}`);
198
+ }
199
+ setConfigValue(result, key, value);
200
+ }
201
+ return result;
202
+ }
203
+
204
+ export function loadSFTTomlConfig(filePath: string): SFTTrainerConfig {
205
+ const absolutePath = resolvePath(filePath);
206
+ let fileContents: string;
207
+ try {
208
+ fileContents = readFileSync(absolutePath, 'utf8');
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ throw new SFTConfigError(`Failed to read config at ${absolutePath}: ${message}`);
212
+ }
213
+ let parsedRaw: unknown;
214
+ try {
215
+ parsedRaw = parseToml(fileContents);
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ throw new SFTConfigError(`Failed to parse TOML at ${absolutePath}: ${message}`);
219
+ }
220
+ if (parsedRaw === null || typeof parsedRaw !== 'object' || Array.isArray(parsedRaw)) {
221
+ throw new SFTConfigError(`Expected table at ${absolutePath}`);
222
+ }
223
+ const parsed = parsedRaw as Record<string, unknown>;
224
+ const normalized = normalizeTomlRecord(parsed);
225
+ return mergeSFTConfig(getDefaultSFTConfig(), normalized);
226
+ }
227
+
228
+ export function applySFTOverrides(config: SFTTrainerConfig, overrides: string[]): SFTTrainerConfig {
229
+ if (!overrides.length) {
230
+ return { ...config };
231
+ }
232
+ const accumulated: Partial<SFTTrainerConfig> = {};
233
+ for (const entry of overrides) {
234
+ const idx = entry.indexOf('=');
235
+ if (idx === -1) {
236
+ throw new SFTConfigError(`Invalid override "${entry}", expected key=value format`);
237
+ }
238
+ const rawKey = entry.slice(0, idx).trim();
239
+ const rawValue = entry.slice(idx + 1).trim();
240
+ // Accept both snake_case and camelCase overrides
241
+ const key = camelCase(rawKey);
242
+ if (!isConfigKey(key)) {
243
+ throw new SFTConfigError(`Unknown configuration key in override: ${rawKey}`);
244
+ }
245
+ setConfigValue(accumulated, key, coerceValue(key, rawValue));
246
+ }
247
+ return mergeSFTConfig(config, accumulated);
248
+ }
249
+
250
+ export { DEFAULT_SFT_CONFIG };