@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.
- package/package.json +6 -4
- package/src/data/dataset.ts +193 -0
- package/src/data/sft-dataset.ts +562 -0
- package/src/index.ts +139 -0
- package/src/trainers/grpo-trainer.ts +2151 -0
- package/src/trainers/sft-config.ts +250 -0
- package/src/trainers/sft-trainer.ts +679 -0
- package/src/trainers/training-logger.ts +830 -0
- package/src/types.ts +70 -0
- package/src/utils/path-security.ts +88 -0
- package/src/utils/xml-parser.ts +209 -0
|
@@ -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 };
|