ovinduG commited on
Commit
191f79e
·
verified ·
1 Parent(s): a06ad77

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: unsloth/gemma-4-E4B
3
+ library_name: peft
4
+ pipeline_tag: text-generation
5
+ tags:
6
+ - base_model:adapter:unsloth/gemma-4-E4B
7
+ - lora
8
+ - sft
9
+ - transformers
10
+ - trl
11
+ - unsloth
12
+ ---
13
+
14
+ # Model Card for Model ID
15
+
16
+ <!-- Provide a quick summary of what the model is/does. -->
17
+
18
+
19
+
20
+ ## Model Details
21
+
22
+ ### Model Description
23
+
24
+ <!-- Provide a longer summary of what this model is. -->
25
+
26
+
27
+
28
+ - **Developed by:** [More Information Needed]
29
+ - **Funded by [optional]:** [More Information Needed]
30
+ - **Shared by [optional]:** [More Information Needed]
31
+ - **Model type:** [More Information Needed]
32
+ - **Language(s) (NLP):** [More Information Needed]
33
+ - **License:** [More Information Needed]
34
+ - **Finetuned from model [optional]:** [More Information Needed]
35
+
36
+ ### Model Sources [optional]
37
+
38
+ <!-- Provide the basic links for the model. -->
39
+
40
+ - **Repository:** [More Information Needed]
41
+ - **Paper [optional]:** [More Information Needed]
42
+ - **Demo [optional]:** [More Information Needed]
43
+
44
+ ## Uses
45
+
46
+ <!-- Address questions around how the model is intended to be used, including the foreseeable users of the model and those affected by the model. -->
47
+
48
+ ### Direct Use
49
+
50
+ <!-- This section is for the model use without fine-tuning or plugging into a larger ecosystem/app. -->
51
+
52
+ [More Information Needed]
53
+
54
+ ### Downstream Use [optional]
55
+
56
+ <!-- This section is for the model use when fine-tuned for a task, or when plugged into a larger ecosystem/app -->
57
+
58
+ [More Information Needed]
59
+
60
+ ### Out-of-Scope Use
61
+
62
+ <!-- This section addresses misuse, malicious use, and uses that the model will not work well for. -->
63
+
64
+ [More Information Needed]
65
+
66
+ ## Bias, Risks, and Limitations
67
+
68
+ <!-- This section is meant to convey both technical and sociotechnical limitations. -->
69
+
70
+ [More Information Needed]
71
+
72
+ ### Recommendations
73
+
74
+ <!-- This section is meant to convey recommendations with respect to the bias, risk, and technical limitations. -->
75
+
76
+ Users (both direct and downstream) should be made aware of the risks, biases and limitations of the model. More information needed for further recommendations.
77
+
78
+ ## How to Get Started with the Model
79
+
80
+ Use the code below to get started with the model.
81
+
82
+ [More Information Needed]
83
+
84
+ ## Training Details
85
+
86
+ ### Training Data
87
+
88
+ <!-- This should link to a Dataset Card, perhaps with a short stub of information on what the training data is all about as well as documentation related to data pre-processing or additional filtering. -->
89
+
90
+ [More Information Needed]
91
+
92
+ ### Training Procedure
93
+
94
+ <!-- This relates heavily to the Technical Specifications. Content here should link to that section when it is relevant to the training procedure. -->
95
+
96
+ #### Preprocessing [optional]
97
+
98
+ [More Information Needed]
99
+
100
+
101
+ #### Training Hyperparameters
102
+
103
+ - **Training regime:** [More Information Needed] <!--fp32, fp16 mixed precision, bf16 mixed precision, bf16 non-mixed precision, fp16 non-mixed precision, fp8 mixed precision -->
104
+
105
+ #### Speeds, Sizes, Times [optional]
106
+
107
+ <!-- This section provides information about throughput, start/end time, checkpoint size if relevant, etc. -->
108
+
109
+ [More Information Needed]
110
+
111
+ ## Evaluation
112
+
113
+ <!-- This section describes the evaluation protocols and provides the results. -->
114
+
115
+ ### Testing Data, Factors & Metrics
116
+
117
+ #### Testing Data
118
+
119
+ <!-- This should link to a Dataset Card if possible. -->
120
+
121
+ [More Information Needed]
122
+
123
+ #### Factors
124
+
125
+ <!-- These are the things the evaluation is disaggregating by, e.g., subpopulations or domains. -->
126
+
127
+ [More Information Needed]
128
+
129
+ #### Metrics
130
+
131
+ <!-- These are the evaluation metrics being used, ideally with a description of why. -->
132
+
133
+ [More Information Needed]
134
+
135
+ ### Results
136
+
137
+ [More Information Needed]
138
+
139
+ #### Summary
140
+
141
+
142
+
143
+ ## Model Examination [optional]
144
+
145
+ <!-- Relevant interpretability work for the model goes here -->
146
+
147
+ [More Information Needed]
148
+
149
+ ## Environmental Impact
150
+
151
+ <!-- Total emissions (in grams of CO2eq) and additional considerations, such as electricity usage, go here. Edit the suggested text below accordingly -->
152
+
153
+ Carbon emissions can be estimated using the [Machine Learning Impact calculator](https://mlco2.github.io/impact#compute) presented in [Lacoste et al. (2019)](https://arxiv.org/abs/1910.09700).
154
+
155
+ - **Hardware Type:** [More Information Needed]
156
+ - **Hours used:** [More Information Needed]
157
+ - **Cloud Provider:** [More Information Needed]
158
+ - **Compute Region:** [More Information Needed]
159
+ - **Carbon Emitted:** [More Information Needed]
160
+
161
+ ## Technical Specifications [optional]
162
+
163
+ ### Model Architecture and Objective
164
+
165
+ [More Information Needed]
166
+
167
+ ### Compute Infrastructure
168
+
169
+ [More Information Needed]
170
+
171
+ #### Hardware
172
+
173
+ [More Information Needed]
174
+
175
+ #### Software
176
+
177
+ [More Information Needed]
178
+
179
+ ## Citation [optional]
180
+
181
+ <!-- If there is a paper or blog post introducing the model, the APA and Bibtex information for that should go in this section. -->
182
+
183
+ **BibTeX:**
184
+
185
+ [More Information Needed]
186
+
187
+ **APA:**
188
+
189
+ [More Information Needed]
190
+
191
+ ## Glossary [optional]
192
+
193
+ <!-- If relevant, include terms and calculations in this section that can help readers understand the model or model card. -->
194
+
195
+ [More Information Needed]
196
+
197
+ ## More Information [optional]
198
+
199
+ [More Information Needed]
200
+
201
+ ## Model Card Authors [optional]
202
+
203
+ [More Information Needed]
204
+
205
+ ## Model Card Contact
206
+
207
+ [More Information Needed]
208
+ ### Framework versions
209
+
210
+ - PEFT 0.21.2
adapter_config.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": {
6
+ "base_model_class": "Gemma4ForConditionalGeneration",
7
+ "parent_library": "unsloth_compiled_module_gemma4",
8
+ "unsloth_fixed": true
9
+ },
10
+ "base_model_name_or_path": "unsloth/gemma-4-E4B",
11
+ "bias": "none",
12
+ "corda_config": null,
13
+ "ensure_weight_tying": false,
14
+ "eva_config": null,
15
+ "exclude_modules": null,
16
+ "fan_in_fan_out": false,
17
+ "inference_mode": true,
18
+ "init_lora_weights": true,
19
+ "kasa_config": null,
20
+ "layer_replication": null,
21
+ "layers_pattern": null,
22
+ "layers_to_transform": null,
23
+ "loftq_config": {},
24
+ "lora_alpha": 32,
25
+ "lora_bias": false,
26
+ "lora_dropout": 0,
27
+ "lora_ga_config": null,
28
+ "megatron_config": null,
29
+ "megatron_core": "megatron.core",
30
+ "modules_to_save": null,
31
+ "monteclora_config": null,
32
+ "peft_type": "LORA",
33
+ "peft_version": "0.21.2",
34
+ "qalora_group_size": 16,
35
+ "r": 32,
36
+ "rank_pattern": {},
37
+ "revision": null,
38
+ "target_modules": "(?:.*?(?:language|text).*?(?:self_attn|attention|attn|mixer|mlp|feed_forward|ffn|dense|mixer).*?\\.(?:k_proj|q_proj|v_proj|o_proj|gate_proj|up_proj|down_proj|per_layer_input_gate|per_layer_projection|linear|embedding_projection|relative_k_proj))|(?:\\bmodel\\.layers\\.[\\d]{1,}(?!.*\\.experts\\.\\d)\\..*?(?:self_attn|attention|attn|mixer|mlp|feed_forward|ffn|dense|mixer).*?\\.(?:(?:k_proj|q_proj|v_proj|o_proj|gate_proj|up_proj|down_proj|per_layer_input_gate|per_layer_projection|linear|embedding_projection|relative_k_proj)))",
39
+ "target_parameters": null,
40
+ "task_type": "CAUSAL_LM",
41
+ "trainable_token_indices": null,
42
+ "use_bdlora": null,
43
+ "use_dora": false,
44
+ "use_qalora": false,
45
+ "use_rslora": false,
46
+ "velora_config": null
47
+ }
adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:595aa89a198262b8de9332d3de1fe563678ee1566635d9c463a2b3c65154f2a9
3
+ size 293689248
processor_config.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_ms_per_token": 40,
3
+ "audio_seq_length": 750,
4
+ "feature_extractor": {
5
+ "dither": 0.0,
6
+ "feature_extractor_type": "Gemma4AudioFeatureExtractor",
7
+ "feature_size": 128,
8
+ "fft_length": 512,
9
+ "fft_overdrive": false,
10
+ "frame_length": 320,
11
+ "hop_length": 160,
12
+ "input_scale_factor": 1.0,
13
+ "max_frequency": 8000.0,
14
+ "mel_floor": 0.001,
15
+ "min_frequency": 0.0,
16
+ "padding_side": "right",
17
+ "padding_value": 0.0,
18
+ "per_bin_mean": null,
19
+ "per_bin_stddev": null,
20
+ "preemphasis": 0.0,
21
+ "preemphasis_htk_flavor": true,
22
+ "return_attention_mask": true,
23
+ "sampling_rate": 16000
24
+ },
25
+ "image_processor": {
26
+ "do_convert_rgb": true,
27
+ "do_normalize": false,
28
+ "do_rescale": true,
29
+ "do_resize": true,
30
+ "image_mean": [
31
+ 0.0,
32
+ 0.0,
33
+ 0.0
34
+ ],
35
+ "image_processor_type": "Gemma4ImageProcessor",
36
+ "image_seq_length": 280,
37
+ "image_std": [
38
+ 1.0,
39
+ 1.0,
40
+ 1.0
41
+ ],
42
+ "max_soft_tokens": 280,
43
+ "patch_size": 16,
44
+ "pooling_kernel_size": 3,
45
+ "resample": 3,
46
+ "rescale_factor": 0.00392156862745098
47
+ },
48
+ "image_seq_length": 280,
49
+ "processor_class": "Gemma4Processor",
50
+ "video_processor": {
51
+ "do_convert_rgb": true,
52
+ "do_normalize": true,
53
+ "do_rescale": true,
54
+ "do_resize": true,
55
+ "do_sample_frames": true,
56
+ "image_mean": [
57
+ 0.0,
58
+ 0.0,
59
+ 0.0
60
+ ],
61
+ "image_std": [
62
+ 1.0,
63
+ 1.0,
64
+ 1.0
65
+ ],
66
+ "max_soft_tokens": 70,
67
+ "num_frames": 32,
68
+ "patch_size": 16,
69
+ "pooling_kernel_size": 3,
70
+ "resample": 3,
71
+ "rescale_factor": 0.00392156862745098,
72
+ "return_metadata": false,
73
+ "video_processor_type": "Gemma4VideoProcessor"
74
+ }
75
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:12bac982b793c44b03d52a250a9f0d0b666813da566b910c24a6da0695fd11e6
3
+ size 32170070
tokenizer_config.json ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_token": "<|audio|>",
3
+ "backend": "tokenizers",
4
+ "boa_token": "<|audio>",
5
+ "boi_token": "<|image>",
6
+ "bos_token": "<bos>",
7
+ "eoa_token": "<audio|>",
8
+ "eoc_token": "<channel|>",
9
+ "eoi_token": "<image|>",
10
+ "eos_token": "<eos>",
11
+ "eot_token": "<turn|>",
12
+ "escape_token": "<|\"|>",
13
+ "etc_token": "<tool_call|>",
14
+ "etd_token": "<tool|>",
15
+ "etr_token": "<tool_response|>",
16
+ "extra_special_tokens": [
17
+ "<|video|>"
18
+ ],
19
+ "image_token": "<|image|>",
20
+ "is_local": false,
21
+ "mask_token": "<mask>",
22
+ "model_max_length": 1000000000000000019884624838656,
23
+ "model_specific_special_tokens": {
24
+ "audio_token": "<|audio|>",
25
+ "boa_token": "<|audio>",
26
+ "boi_token": "<|image>",
27
+ "eoa_token": "<audio|>",
28
+ "eoc_token": "<channel|>",
29
+ "eoi_token": "<image|>",
30
+ "eot_token": "<turn|>",
31
+ "escape_token": "<|\"|>",
32
+ "etc_token": "<tool_call|>",
33
+ "etd_token": "<tool|>",
34
+ "etr_token": "<tool_response|>",
35
+ "image_token": "<|image|>",
36
+ "soc_token": "<|channel>",
37
+ "sot_token": "<|turn>",
38
+ "stc_token": "<|tool_call>",
39
+ "std_token": "<|tool>",
40
+ "str_token": "<|tool_response>",
41
+ "think_token": "<|think|>"
42
+ },
43
+ "pad_token": "<pad>",
44
+ "padding_side": "right",
45
+ "processor_class": "Gemma4Processor",
46
+ "soc_token": "<|channel>",
47
+ "sot_token": "<|turn>",
48
+ "stc_token": "<|tool_call>",
49
+ "std_token": "<|tool>",
50
+ "str_token": "<|tool_response>",
51
+ "think_token": "<|think|>",
52
+ "tokenizer_class": "GemmaTokenizer",
53
+ "unk_token": "<unk>",
54
+ "added_tokens_decoder": {
55
+ "0": {
56
+ "content": "<pad>",
57
+ "single_word": false,
58
+ "lstrip": false,
59
+ "rstrip": false,
60
+ "normalized": false,
61
+ "special": true
62
+ },
63
+ "1": {
64
+ "content": "<eos>",
65
+ "single_word": false,
66
+ "lstrip": false,
67
+ "rstrip": false,
68
+ "normalized": false,
69
+ "special": true
70
+ },
71
+ "2": {
72
+ "content": "<bos>",
73
+ "single_word": false,
74
+ "lstrip": false,
75
+ "rstrip": false,
76
+ "normalized": false,
77
+ "special": true
78
+ },
79
+ "3": {
80
+ "content": "<unk>",
81
+ "single_word": false,
82
+ "lstrip": false,
83
+ "rstrip": false,
84
+ "normalized": false,
85
+ "special": true
86
+ },
87
+ "4": {
88
+ "content": "<mask>",
89
+ "single_word": false,
90
+ "lstrip": false,
91
+ "rstrip": false,
92
+ "normalized": false,
93
+ "special": true
94
+ },
95
+ "46": {
96
+ "content": "<|tool>",
97
+ "single_word": false,
98
+ "lstrip": false,
99
+ "rstrip": false,
100
+ "normalized": false,
101
+ "special": true
102
+ },
103
+ "47": {
104
+ "content": "<tool|>",
105
+ "single_word": false,
106
+ "lstrip": false,
107
+ "rstrip": false,
108
+ "normalized": false,
109
+ "special": true
110
+ },
111
+ "48": {
112
+ "content": "<|tool_call>",
113
+ "single_word": false,
114
+ "lstrip": false,
115
+ "rstrip": false,
116
+ "normalized": false,
117
+ "special": true
118
+ },
119
+ "49": {
120
+ "content": "<tool_call|>",
121
+ "single_word": false,
122
+ "lstrip": false,
123
+ "rstrip": false,
124
+ "normalized": false,
125
+ "special": true
126
+ },
127
+ "50": {
128
+ "content": "<|tool_response>",
129
+ "single_word": false,
130
+ "lstrip": false,
131
+ "rstrip": false,
132
+ "normalized": false,
133
+ "special": true
134
+ },
135
+ "51": {
136
+ "content": "<tool_response|>",
137
+ "single_word": false,
138
+ "lstrip": false,
139
+ "rstrip": false,
140
+ "normalized": false,
141
+ "special": true
142
+ },
143
+ "52": {
144
+ "content": "<|\"|>",
145
+ "single_word": false,
146
+ "lstrip": false,
147
+ "rstrip": false,
148
+ "normalized": false,
149
+ "special": true
150
+ },
151
+ "98": {
152
+ "content": "<|think|>",
153
+ "single_word": false,
154
+ "lstrip": false,
155
+ "rstrip": false,
156
+ "normalized": false,
157
+ "special": true
158
+ },
159
+ "100": {
160
+ "content": "<|channel>",
161
+ "single_word": false,
162
+ "lstrip": false,
163
+ "rstrip": false,
164
+ "normalized": false,
165
+ "special": true
166
+ },
167
+ "101": {
168
+ "content": "<channel|>",
169
+ "single_word": false,
170
+ "lstrip": false,
171
+ "rstrip": false,
172
+ "normalized": false,
173
+ "special": true
174
+ },
175
+ "105": {
176
+ "content": "<|turn>",
177
+ "single_word": false,
178
+ "lstrip": false,
179
+ "rstrip": false,
180
+ "normalized": false,
181
+ "special": true
182
+ },
183
+ "106": {
184
+ "content": "<turn|>",
185
+ "single_word": false,
186
+ "lstrip": false,
187
+ "rstrip": false,
188
+ "normalized": false,
189
+ "special": true
190
+ },
191
+ "255999": {
192
+ "content": "<|image>",
193
+ "single_word": false,
194
+ "lstrip": false,
195
+ "rstrip": false,
196
+ "normalized": false,
197
+ "special": true
198
+ },
199
+ "256000": {
200
+ "content": "<|audio>",
201
+ "single_word": false,
202
+ "lstrip": false,
203
+ "rstrip": false,
204
+ "normalized": false,
205
+ "special": true
206
+ },
207
+ "258880": {
208
+ "content": "<|image|>",
209
+ "single_word": false,
210
+ "lstrip": false,
211
+ "rstrip": false,
212
+ "normalized": false,
213
+ "special": true
214
+ },
215
+ "258881": {
216
+ "content": "<|audio|>",
217
+ "single_word": false,
218
+ "lstrip": false,
219
+ "rstrip": false,
220
+ "normalized": false,
221
+ "special": true
222
+ },
223
+ "258882": {
224
+ "content": "<image|>",
225
+ "single_word": false,
226
+ "lstrip": false,
227
+ "rstrip": false,
228
+ "normalized": false,
229
+ "special": true
230
+ },
231
+ "258883": {
232
+ "content": "<audio|>",
233
+ "single_word": false,
234
+ "lstrip": false,
235
+ "rstrip": false,
236
+ "normalized": false,
237
+ "special": true
238
+ },
239
+ "258884": {
240
+ "content": "<|video|>",
241
+ "single_word": false,
242
+ "lstrip": false,
243
+ "rstrip": false,
244
+ "normalized": false,
245
+ "special": true
246
+ }
247
+ }
248
+ }
training/make_predictions.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Produce the shared-task submission: predictions.zip containing predictions.jsonl.
4
+
5
+ Required line format (one JSON object per question):
6
+ {"q_no":1,"subject":"Arts","category":"humanities","level":"easy","answer":"2"}
7
+
8
+ `answer` is a STRING holding the 1-based option number.
9
+
10
+ python make_predictions.py \
11
+ --data /workspace/data/TEST \
12
+ --model /workspace/outputs/mcq_sft_base_v2/best_adapter \
13
+ --out predictions.jsonl --zip predictions.zip
14
+
15
+ --data may be either a single folder of *.json files, or a root containing easy/ medium/ hard/
16
+ subfolders; the level comes from metadata.difficulty, falling back to the folder name.
17
+
18
+ Scoring: for every option, score the joint tokenisation of prompt+" N" and take the highest total
19
+ log-probability ("loglik"). This is deterministic, always yields a valid 1..N answer, and measured
20
+ highest on TEST/easy. Because the task is submitted as PREDICTIONS, we are free to choose the
21
+ decoding method - we are not restricted to the organisers' bare-digit logit comparison.
22
+
23
+ Reproducibility: the organisers re-run submitted code on their own hardware and only verified runs
24
+ enter the final ranking. Nothing here samples; seeds are fixed and generation is not used. Keep this
25
+ file, the adapter/model reference and the prompt builder together in the submitted code.
26
+ """
27
+ import argparse
28
+ import glob
29
+ import json
30
+ import os
31
+ import re
32
+ import random
33
+ import sys
34
+ import time
35
+ import zipfile
36
+
37
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
38
+
39
+ # --- the organisers' prompt, verbatim from their util_prompt.py -------------------------------
40
+ SINHALA_INSTRUCTIONS = {
41
+ "easy": "පහත ප්‍රශ්නයට 1, 2, 3, 4 යන පිළිතුරුවලින් නිවැරදි හෝ ඉතාමත් ගැළපෙන පිළිතුර තෝරන්න.\n\n",
42
+ "medium": "පහත ප්‍රශ්නයට 1, 2, 3, 4 යන පිළිතුරුවලින් නිවැරදි හෝ ඉතාමත් ගැළපෙන පිළිතුර තෝරාගන්න.\n\n",
43
+ "hard": "පහත ප්‍රශ්නයට 1, 2, 3, 4, 5 යන පිළිතුරුවලින් නිවැරදි හෝ ඉතාමත් ගැළපෙන පිළිතුර තෝරාගන්න.\n\n",
44
+ }
45
+ ENGLISH_INSTRUCTIONS = {
46
+ "easy": "Choose the correct or most appropriate answer from 1, 2, 3, 4 for the question below.\n\n",
47
+ "medium": "Choose the correct or most appropriate answer from 1, 2, 3, 4 for the question below.\n\n",
48
+ "hard": "Select the correct answer from 1, 2, 3, 4, 5 for the given question.\n\n",
49
+ }
50
+ LEVELS = ("easy", "medium", "hard")
51
+
52
+ # Prompt variants to ensemble over. Each is a real configuration the organisers' harness supports
53
+ # (sinhala/english x intro true/false), plus option rotations to cancel answer-position bias.
54
+ # "rot" rotates the DISPLAYED option order; predictions are mapped back to the original indices.
55
+ VARIANTS = {
56
+ "si_intro": {"lang": "si", "intro": True, "rot": 0},
57
+ "si_nointro": {"lang": "si", "intro": False, "rot": 0},
58
+ "en_intro": {"lang": "en", "intro": True, "rot": 0},
59
+ "si_rot1": {"lang": "si", "intro": True, "rot": 1},
60
+ "si_rot2": {"lang": "si", "intro": True, "rot": 2},
61
+ }
62
+ DEFAULT_ENSEMBLE = ["si_intro", "si_nointro", "en_intro", "si_rot1", "si_rot2"]
63
+
64
+
65
+ def fit_numbers(instruction, n):
66
+ """The organisers hard-code "1, 2, 3, 4, 5" in the HARD instruction because the original
67
+ SinhalaMMLU hard split had five options. SinhalaMMLU 2.0 dropped one incorrect option, so hard
68
+ questions now have four. Telling the model to choose from five options when four are shown is
69
+ strictly worse, so the number run is rewritten to match the actual count."""
70
+ return re.sub(r"1,\s*2,\s*3,\s*4(?:,\s*5)?",
71
+ ", ".join(str(i + 1) for i in range(n)), instruction, count=1)
72
+
73
+
74
+ def official_prompt(level, subject_original, question, choices, intro_type=True):
75
+ intro = f"මෙය {subject_original} විෂයයට අදාළ බහුවරණ ප්‍රශ්නයකි.\n" if intro_type else ""
76
+ body = "\n".join(f"{i + 1}. {str(c).strip()}" for i, c in enumerate(choices))
77
+ instr = fit_numbers(SINHALA_INSTRUCTIONS[level], len(choices))
78
+ return intro + instr + f"ප්‍රශ්නය: {question}\n{body}\nපිළිතුර:"
79
+
80
+
81
+ def english_prompt(level, subject, question, choices, intro_type=True):
82
+ """The organisers' prompt with language="english" (their english_intro uses the ENGLISH subject)."""
83
+ intro = (f"This is a multiple choice question related to the subject {subject}. "
84
+ "Given in Sinhala Language.\n") if intro_type else ""
85
+ body = "\n".join(f"{i + 1}. {str(c).strip()}" for i, c in enumerate(choices))
86
+ instr = fit_numbers(ENGLISH_INSTRUCTIONS[level], len(choices))
87
+ return intro + instr + f"Question: {question}\n{body}\nAnswer:"
88
+
89
+
90
+ def rotate(choices, rot):
91
+ """Displayed order; displayed position j holds original option (j + rot) % n."""
92
+ n = len(choices)
93
+ r = rot % n if n else 0
94
+ return choices[r:] + choices[:r], r
95
+
96
+
97
+ def variant_prompt(row, spec):
98
+ shown, r = rotate(row["choices"], spec["rot"])
99
+ if spec["lang"] == "en":
100
+ p = english_prompt(row["level"], row["subject"], row["question"], shown, spec["intro"])
101
+ else:
102
+ p = official_prompt(row["level"], row["subject_original"], row["question"], shown, spec["intro"])
103
+ return p, r, len(shown)
104
+
105
+
106
+ def level_of(item, path, override=""):
107
+ """metadata.difficulty, else a top-level 'level'/'difficulty' field, else the file name or any
108
+ path component containing easy/medium/hard, else 'easy'."""
109
+ if override:
110
+ return override
111
+ md = item.get("metadata") or {}
112
+ for cand in (md.get("difficulty"), md.get("level"), item.get("level"), item.get("difficulty")):
113
+ d = str(cand or "").strip().lower()
114
+ if d in LEVELS:
115
+ return d
116
+ low = path.replace("\\", "/").lower()
117
+ # substring match so "hard.json", "test_hard.jsonl" and ".../hard/..." all work
118
+ hits = [lv for lv in LEVELS if lv in low]
119
+ if len(hits) == 1:
120
+ return hits[0]
121
+ if hits: # e.g. ".../easy/medium_extra.json": trust the last one
122
+ return max(hits, key=low.rfind)
123
+ return "easy"
124
+
125
+
126
+ def read_any(path):
127
+ """A JSON array, a JSONL file, or a dict wrapping the list under a common key."""
128
+ raw = open(path, encoding="utf-8-sig").read().strip()
129
+ if not raw:
130
+ return []
131
+ if raw[0] == "[":
132
+ return json.loads(raw)
133
+ if raw[0] == "{":
134
+ try:
135
+ obj = json.loads(raw)
136
+ except json.JSONDecodeError:
137
+ obj = None
138
+ if isinstance(obj, dict):
139
+ for k in ("data", "questions", "items", "rows", "test"):
140
+ if isinstance(obj.get(k), list):
141
+ return obj[k]
142
+ return [obj]
143
+ # not one object -> treat as JSONL
144
+ out = []
145
+ for line in raw.splitlines():
146
+ line = line.strip()
147
+ if line:
148
+ out.append(json.loads(line))
149
+ return out
150
+
151
+
152
+ def load_items(data_dir, level_override=""):
153
+ if os.path.isfile(data_dir):
154
+ files = [data_dir]
155
+ else:
156
+ files = sorted(set(glob.glob(os.path.join(data_dir, "**", "*.json"), recursive=True))
157
+ | set(glob.glob(os.path.join(data_dir, "**", "*.jsonl"), recursive=True)))
158
+ if not files:
159
+ sys.exit(f"no .json/.jsonl files found under {data_dir}")
160
+ rows, skipped, per_file = [], 0, []
161
+ for path in files:
162
+ try:
163
+ data = read_any(path)
164
+ except Exception as e: # noqa: BLE001
165
+ print(f"[warn] {os.path.basename(path)}: {type(e).__name__}: {e}")
166
+ continue
167
+ if not isinstance(data, list) or not data:
168
+ print(f"[warn] {os.path.basename(path)}: no question list found")
169
+ continue
170
+ before = len(rows)
171
+ for it in data:
172
+ md = it.get("metadata") or {}
173
+ ch = [c for c in (it.get("choices") or []) if str(c).strip()]
174
+ if len(ch) < 2:
175
+ skipped += 1 # never drop silently: these still need a prediction line
176
+ rows.append({
177
+ "file": os.path.basename(path),
178
+ "q_no": it.get("q_no"),
179
+ "subject": it.get("subject"),
180
+ "category": it.get("category"),
181
+ "level": level_of(it, path, level_override),
182
+ "question": str(it.get("question") or "").strip(),
183
+ "choices": ch,
184
+ "subject_original": md.get("subject_original") or it.get("subject"),
185
+ })
186
+ lv = {r["level"] for r in rows[before:]}
187
+ per_file.append((os.path.basename(path), len(rows) - before, sorted(lv)))
188
+ by_level = {}
189
+ for r in rows:
190
+ by_level[r["level"]] = by_level.get(r["level"], 0) + 1
191
+ print(f"[data] {len(rows)} questions from {len(files)} file(s); by level: {by_level}")
192
+ for name, n, lv in per_file:
193
+ print(f" {name:<44} {n:>6} questions level -> {','.join(lv)}")
194
+ missing = [lv for lv in LEVELS if lv not in by_level]
195
+ if missing:
196
+ print(f"[warn] no questions detected for: {missing}. If that is wrong, the level could not be "
197
+ "read from metadata.difficulty or the file name - check the table above.")
198
+ if skipped:
199
+ print(f"[warn] {skipped} question(s) have fewer than 2 usable options; they still get a line")
200
+ return rows
201
+
202
+
203
+ def common_prefix_len(a, b):
204
+ n = 0
205
+ for x, y in zip(a, b):
206
+ if x != y:
207
+ break
208
+ n += 1
209
+ return n
210
+
211
+
212
+ def predict(model, tok, rows, max_len, batch_seqs, variants):
213
+ """Deterministic prompt ensemble. For every (question, variant) the options are scored, converted
214
+ to a log-softmax over the DISPLAYED positions (so variants with different prompt lengths are on a
215
+ common scale), mapped back to the original option order, and summed across variants.
216
+ No sampling, no generation: the same inputs always give the same predictions."""
217
+ import math
218
+ import torch
219
+ model.eval()
220
+ dev = next(model.parameters()).device
221
+ pad = tok.pad_token_id if tok.pad_token_id is not None else 0
222
+
223
+ items, meta = [], {} # (ri, vi, shown_pos, ids, answer_start) ; (ri,vi)->(rot,n)
224
+ for ri, r in enumerate(rows):
225
+ if len(r["choices"]) < 2:
226
+ continue
227
+ for vi, vname in enumerate(variants):
228
+ prompt, rot, n = variant_prompt(r, VARIANTS[vname])
229
+ meta[(ri, vi)] = (rot, n)
230
+ p_ids = tok(prompt)["input_ids"]
231
+ for j in range(n):
232
+ f = tok(prompt + f" {j + 1}")["input_ids"]
233
+ if len(f) > max_len:
234
+ f = f[-max_len:]
235
+ items.append((ri, vi, j, f, max(1, common_prefix_len(p_ids, f))))
236
+
237
+ print(f"[predict] {len(rows)} questions x {len(variants)} variant(s) = {len(items)} candidates")
238
+ scores, t0 = {}, time.time()
239
+ with torch.no_grad():
240
+ for i in range(0, len(items), batch_seqs):
241
+ chunk = items[i:i + batch_seqs]
242
+ L = max(len(x[3]) for x in chunk)
243
+ ids = torch.full((len(chunk), L), pad, dtype=torch.long)
244
+ mask = torch.zeros((len(chunk), L), dtype=torch.long)
245
+ for b, (_, _, _, sq, _) in enumerate(chunk):
246
+ ids[b, :len(sq)] = torch.tensor(sq)
247
+ mask[b, :len(sq)] = 1
248
+ logits = model(input_ids=ids.to(dev), attention_mask=mask.to(dev)).logits
249
+ for b, (ri, vi, j, sq, k) in enumerate(chunk):
250
+ lp = logits[b, k - 1:len(sq) - 1].float().log_softmax(-1)
251
+ tgt = ids[b, k:len(sq)].to(dev)
252
+ scores[(ri, vi, j)] = lp.gather(-1, tgt[:, None]).sum().item()
253
+ del logits
254
+ if (i // max(1, batch_seqs)) % 100 == 0:
255
+ done = min(i + batch_seqs, len(items))
256
+ print(f" scored {done}/{len(items)} ({time.time() - t0:.0f}s)", flush=True)
257
+
258
+ preds, fallback, agree = [], 0, 0
259
+ for ri, r in enumerate(rows):
260
+ n = len(r["choices"])
261
+ if n < 2:
262
+ preds.append(1)
263
+ fallback += 1
264
+ continue
265
+ total, used, per_variant = [0.0] * n, 0, []
266
+ for vi in range(len(variants)):
267
+ if (ri, vi) not in meta:
268
+ continue
269
+ rot, nv = meta[(ri, vi)]
270
+ raw = [scores.get((ri, vi, j)) for j in range(nv)]
271
+ if any(x is None for x in raw):
272
+ continue
273
+ m = max(raw) # log-softmax over displayed positions
274
+ z = m + math.log(sum(math.exp(x - m) for x in raw))
275
+ best_j = max(range(nv), key=lambda j: raw[j])
276
+ per_variant.append(1 + (best_j + rot) % n)
277
+ for j in range(nv):
278
+ total[(j + rot) % n] += raw[j] - z # displayed j == original (j+rot)%n
279
+ used += 1
280
+ if used == 0:
281
+ preds.append(1)
282
+ fallback += 1
283
+ continue
284
+ preds.append(1 + max(range(n), key=lambda i: total[i]))
285
+ if len(set(per_variant)) == 1:
286
+ agree += 1
287
+
288
+ if fallback:
289
+ print(f"[warn] {fallback} question(s) fell back to answer 1")
290
+ if len(variants) > 1:
291
+ print(f"[predict] all variants agreed on {agree}/{len(rows)} ({agree / max(1, len(rows)):.1%}) "
292
+ "- the rest are where the ensemble changes the answer")
293
+ print(f"[predict] done in {time.time() - t0:.0f}s")
294
+ return preds, None
295
+
296
+
297
+ def main():
298
+ ap = argparse.ArgumentParser()
299
+ ap.add_argument("--data", required=True, help="folder of test *.json files, or a root with easy/medium/hard")
300
+ ap.add_argument("--model", required=True, help="adapter dir, merged dir, or HF repo")
301
+ ap.add_argument("--out", default="predictions.jsonl")
302
+ ap.add_argument("--zip", default="predictions.zip", help="'' to skip zipping")
303
+ ap.add_argument("--level", default="", choices=["", *LEVELS], help="force the level for every item")
304
+ ap.add_argument("--max_seq_length", type=int, default=1024)
305
+ ap.add_argument("--eval_batch", type=int, default=16)
306
+ ap.add_argument("--ensemble", nargs="*", default=None,
307
+ help=f"prompt variants to average over (default: {' '.join(DEFAULT_ENSEMBLE)}). "
308
+ f"Choices: {' '.join(VARIANTS)}. Pass 'si_intro' alone for single-prompt scoring.")
309
+ ap.add_argument("--seed", type=int, default=3407)
310
+ ap.add_argument("--dry_run", action="store_true", help="show the prompt and exit, no model load")
311
+ args = ap.parse_args()
312
+ for stream in (sys.stdout, sys.stderr):
313
+ try:
314
+ stream.reconfigure(encoding="utf-8")
315
+ except Exception: # noqa: BLE001
316
+ pass
317
+
318
+ random.seed(args.seed)
319
+ variants = args.ensemble if args.ensemble else list(DEFAULT_ENSEMBLE)
320
+ unknown = [v for v in variants if v not in VARIANTS]
321
+ if unknown:
322
+ sys.exit(f"unknown variant(s) {unknown}; choose from {list(VARIANTS)}")
323
+ print(f"[ensemble] {len(variants)} variant(s): {variants}")
324
+ rows = load_items(args.data, args.level)
325
+
326
+ if args.dry_run:
327
+ r = next((x for x in rows if len(x["choices"]) >= 2), None)
328
+ if r:
329
+ for v in variants:
330
+ prompt, rot, _ = variant_prompt(r, VARIANTS[v])
331
+ print(f"\n--- variant {v} (rot={rot}) ---")
332
+ print(prompt)
333
+ print("--- end ---")
334
+ return
335
+
336
+ import torch
337
+ torch.manual_seed(args.seed)
338
+ from unsloth import FastModel
339
+ print(f"[model] loading {args.model}")
340
+ model, processor = FastModel.from_pretrained(model_name=args.model, dtype=None,
341
+ max_seq_length=args.max_seq_length,
342
+ load_in_4bit=False, full_finetuning=False)
343
+ tok = getattr(processor, "tokenizer", processor)
344
+ if tok.pad_token is None:
345
+ tok.pad_token = tok.eos_token
346
+
347
+ preds, _ = predict(model, tok, rows, args.max_seq_length, args.eval_batch, variants)
348
+
349
+ with open(args.out, "w", encoding="utf-8") as f:
350
+ for r, p in zip(rows, preds):
351
+ f.write(json.dumps({"q_no": r["q_no"], "subject": r["subject"],
352
+ "category": r["category"], "level": r["level"],
353
+ "answer": str(p)}, ensure_ascii=False) + "\n")
354
+ print(f"[out] {len(rows)} lines -> {args.out}")
355
+
356
+ # self-checks: the submission is rejected or mis-scored if any of these are wrong
357
+ lines = [json.loads(l) for l in open(args.out, encoding="utf-8") if l.strip()]
358
+ assert len(lines) == len(rows), "line count does not match the question count"
359
+ bad_keys = [i for i, d in enumerate(lines)
360
+ if set(d) != {"q_no", "subject", "category", "level", "answer"}]
361
+ bad_ans = [i for i, d in enumerate(lines)
362
+ if not (isinstance(d["answer"], str) and d["answer"] in {"1", "2", "3", "4", "5"})]
363
+ bad_lvl = [i for i, d in enumerate(lines) if d["level"] not in LEVELS]
364
+ for name, bad in [("wrong key set", bad_keys), ("answer not a '1'..'5' string", bad_ans),
365
+ ("level not easy/medium/hard", bad_lvl)]:
366
+ print(f"[check] {name}: {len(bad)}" + (f" e.g. line {bad[0] + 1}" if bad else " OK"))
367
+ dist = {}
368
+ for d in lines:
369
+ dist[d["answer"]] = dist.get(d["answer"], 0) + 1
370
+ print(f"[check] answer distribution: {dict(sorted(dist.items()))}")
371
+ dup = len(lines) - len({(d["level"], d["subject"], d["q_no"]) for d in lines})
372
+ print(f"[check] duplicate (level, subject, q_no) keys: {dup}")
373
+ if bad_keys or bad_ans or bad_lvl:
374
+ sys.exit("fix the problems above before submitting")
375
+
376
+ if args.zip:
377
+ with zipfile.ZipFile(args.zip, "w", zipfile.ZIP_DEFLATED) as z:
378
+ z.write(args.out, arcname="predictions.jsonl") # must sit at the archive root
379
+ with zipfile.ZipFile(args.zip) as z:
380
+ print(f"[zip] {args.zip} contains: {z.namelist()}")
381
+ print("\nFirst two lines:")
382
+ for l in open(args.out, encoding="utf-8").readlines()[:2]:
383
+ print(" " + l.rstrip())
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()
training/results_v3b_final_easy.json ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "/workspace/outputs/mcq_sft_base_v3b/final_adapter",
3
+ "data": "/workspace/data/TEST/easy",
4
+ "n_questions": 1850,
5
+ "loglik": {
6
+ "accuracy": 0.5756756756756757,
7
+ "n": 1850,
8
+ "no_prediction": 0,
9
+ "by_category": {
10
+ "humanities": 0.5327022375215146,
11
+ "social_science": 0.7127371273712737,
12
+ "language": 0.5096774193548387,
13
+ "stem": 0.6341463414634146
14
+ },
15
+ "by_subject": {
16
+ "Arts": 0.49,
17
+ "Buddhism": 0.5,
18
+ "Catholicism": 0.6363636363636364,
19
+ "Christianity": 0.7153846153846154,
20
+ "Civics": 0.7398373983739838,
21
+ "Eastern_music": 1.0,
22
+ "Eastern music": 0.44366197183098594,
23
+ "Geography": 0.646551724137931,
24
+ "Health and Physical Science": 0.7461538461538462,
25
+ "History": 0.47019867549668876,
26
+ "Islam": 0.6759259259259259,
27
+ "Sinhala_language_and_literature": 0.0,
28
+ "Sinhala language and literature": 0.512987012987013,
29
+ "dancing": 0.3055555555555556,
30
+ "drama_and_theatre": 0.0,
31
+ "drama and Theatre ": 0.5590551181102362,
32
+ "science": 0.6341463414634146
33
+ }
34
+ },
35
+ "official": {
36
+ "accuracy": 0.5708108108108109,
37
+ "n": 1850,
38
+ "no_prediction": 0,
39
+ "by_category": {
40
+ "humanities": 0.5258175559380379,
41
+ "social_science": 0.7046070460704607,
42
+ "language": 0.5419354838709678,
43
+ "stem": 0.6158536585365854
44
+ },
45
+ "by_subject": {
46
+ "Arts": 0.48,
47
+ "Buddhism": 0.47530864197530864,
48
+ "Catholicism": 0.6363636363636364,
49
+ "Christianity": 0.7384615384615385,
50
+ "Civics": 0.7398373983739838,
51
+ "Eastern_music": 1.0,
52
+ "Eastern music": 0.4295774647887324,
53
+ "Geography": 0.6379310344827587,
54
+ "Health and Physical Science": 0.7307692307692307,
55
+ "History": 0.4768211920529801,
56
+ "Islam": 0.6388888888888888,
57
+ "Sinhala_language_and_literature": 0.0,
58
+ "Sinhala language and literature": 0.5454545454545454,
59
+ "dancing": 0.3148148148148148,
60
+ "drama_and_theatre": 0.0,
61
+ "drama and Theatre ": 0.5433070866141733,
62
+ "science": 0.6158536585365854
63
+ }
64
+ }
65
+ }
training/sft_gemma4_mcq.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Supervised fine-tuning of Gemma 4 E4B for Sinhala multiple-choice QA (SinhalaMMLU-style), Unsloth + LoRA.
4
+
5
+ Input: the prompt/completion files made by `mcq_dataset.py build-sft` (plain format, with a `candidates` field).
6
+ The loss is computed ONLY on the answer tokens. Accuracy is measured separately by scoring every answer
7
+ option on the held-out validation file (before training = zero-shot baseline, then after every epoch).
8
+
9
+ # base model
10
+ python sft_gemma4_mcq.py --train sft_train.jsonl --val sft_val.jsonl \
11
+ --model unsloth/gemma-4-E4B --out /workspace/outputs/mcq_sft_base
12
+ # CPT model (local merged folder from the earlier run)
13
+ python sft_gemma4_mcq.py --train sft_train.jsonl --val sft_val.jsonl \
14
+ --model /workspace/outputs/gemma4_e4b_sinhala_cpt/merged_16bit --out /workspace/outputs/mcq_sft_cpt
15
+ # zero-shot baseline only (no training)
16
+ python sft_gemma4_mcq.py --val sft_val.jsonl --model unsloth/gemma-4-E4B --eval_only
17
+
18
+ Outputs: <out>/best_adapter (best validation accuracy), <out>/final_adapter, <out>/history.json
19
+ To merge + upload the best adapter: python upload_to_hf.py --adapter_dir <out>/best_adapter --make_merged ...
20
+ """
21
+ import argparse
22
+ import inspect
23
+ import json
24
+ import os
25
+ import random
26
+ import sys
27
+ import time
28
+
29
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
30
+
31
+
32
+ # ------------------------------------------------------------------ helpers (no GPU needed)
33
+ def load_rows(path):
34
+ rows = [json.loads(l) for l in open(path, encoding="utf-8") if l.strip()]
35
+ for r in rows[:3]:
36
+ if not isinstance(r.get("prompt"), str) or not isinstance(r.get("candidates"), list):
37
+ sys.exit(f"{path}: need plain prompt/completion rows with a 'candidates' list "
38
+ "(re-run mcq_dataset.py build-sft without --format chat)")
39
+ return rows
40
+
41
+
42
+ def common_prefix_len(a, b):
43
+ n = 0
44
+ for x, y in zip(a, b):
45
+ if x != y:
46
+ break
47
+ n += 1
48
+ return n
49
+
50
+
51
+ def encode_example(tok, prompt, completion, add_eos=True):
52
+ """Tokenise prompt+completion jointly (as a test harness would); mask everything before the first
53
+ token that differs from the prompt-only tokenisation. Returns input_ids, labels."""
54
+ p_ids = tok(prompt)["input_ids"]
55
+ f_ids = tok(prompt + completion)["input_ids"]
56
+ k = max(1, common_prefix_len(p_ids, f_ids))
57
+ ids = list(f_ids) + ([tok.eos_token_id] if add_eos and tok.eos_token_id is not None else [])
58
+ labels = [-100] * k + ids[k:]
59
+ return ids, labels
60
+
61
+
62
+ def evaluate(model, tok, rows, max_len=1024, limit=0, batch_seqs=16, verbose=True):
63
+ """Accuracy of picking the option whose answer tokens get the highest total log-probability.
64
+ Every (question, candidate) pair is scored; pairs are batched together for speed."""
65
+ import torch
66
+ was_training = model.training
67
+ model.eval()
68
+ dev = next(model.parameters()).device
69
+ pad = tok.pad_token_id if tok.pad_token_id is not None else 0
70
+ use = rows[:limit] if limit else rows
71
+ t0 = time.time()
72
+
73
+ items, skipped = [], 0 # (row_idx, cand_idx, token_ids, answer_start)
74
+ for ri, r in enumerate(use):
75
+ p_ids = tok(r["prompt"])["input_ids"]
76
+ seqs, starts = [], []
77
+ for c in r["candidates"]:
78
+ f = tok(r["prompt"] + c)["input_ids"]
79
+ seqs.append(f)
80
+ starts.append(max(1, common_prefix_len(p_ids, f)))
81
+ if max(len(x) for x in seqs) > max_len:
82
+ skipped += 1
83
+ continue
84
+ for ci, (sq, k) in enumerate(zip(seqs, starts)):
85
+ items.append((ri, ci, sq, k))
86
+
87
+ scores = {}
88
+ with torch.no_grad():
89
+ for i in range(0, len(items), batch_seqs):
90
+ chunk = items[i:i + batch_seqs]
91
+ L = max(len(x[2]) for x in chunk)
92
+ ids = torch.full((len(chunk), L), pad, dtype=torch.long)
93
+ mask = torch.zeros((len(chunk), L), dtype=torch.long)
94
+ for b, (_, _, sq, _) in enumerate(chunk):
95
+ ids[b, :len(sq)] = torch.tensor(sq)
96
+ mask[b, :len(sq)] = 1
97
+ logits = model(input_ids=ids.to(dev), attention_mask=mask.to(dev)).logits
98
+ for b, (ri, ci, sq, k) in enumerate(chunk):
99
+ lp = logits[b, k - 1:len(sq) - 1].float().log_softmax(-1)
100
+ tgt = ids[b, k:len(sq)].to(dev)
101
+ scores[(ri, ci)] = lp.gather(-1, tgt[:, None]).sum().item()
102
+ del logits
103
+
104
+ n = correct = 0
105
+ by_subj, gold_pos = {}, {}
106
+ for ri, r in enumerate(use):
107
+ cs = [scores.get((ri, ci)) for ci in range(len(r["candidates"]))]
108
+ if any(c is None for c in cs):
109
+ continue
110
+ pred = max(range(len(cs)), key=lambda i: cs[i])
111
+ gold = r["candidates"].index(r["completion"])
112
+ n += 1
113
+ correct += int(pred == gold)
114
+ gold_pos[gold] = gold_pos.get(gold, 0) + 1
115
+ sj = r.get("subject") or "?"
116
+ c_, t_ = by_subj.get(sj, (0, 0))
117
+ by_subj[sj] = (c_ + int(pred == gold), t_ + 1)
118
+
119
+ if was_training:
120
+ model.train()
121
+ acc = correct / max(1, n)
122
+ majority = max(gold_pos.values()) / max(1, n) if gold_pos else 0.0
123
+ if verbose:
124
+ print(f"[eval] accuracy {acc:.4f} ({correct}/{n}); always-guess-most-common-position {majority:.4f}; "
125
+ f"skipped(too long) {skipped}; {time.time() - t0:.0f}s", flush=True)
126
+ return {"acc": acc, "n": n, "majority_baseline": majority,
127
+ "by_subject": {k: round(v[0] / v[1], 4) for k, v in sorted(by_subj.items()) if v[1] >= 20}}
128
+
129
+
130
+ # ------------------------------------------------------------------ main
131
+ def main():
132
+ ap = argparse.ArgumentParser()
133
+ ap.add_argument("--train", default="")
134
+ ap.add_argument("--val", required=True)
135
+ ap.add_argument("--model", default="unsloth/gemma-4-E4B")
136
+ ap.add_argument("--out", default="/workspace/outputs/mcq_sft")
137
+ ap.add_argument("--max_seq_length", type=int, default=1024)
138
+ ap.add_argument("--lora_r", type=int, default=32)
139
+ ap.add_argument("--lora_alpha", type=int, default=32)
140
+ ap.add_argument("--lr", type=float, default=1e-4)
141
+ ap.add_argument("--epochs", type=float, default=3.0)
142
+ ap.add_argument("--max_steps", type=int, default=-1, help=">0 overrides epochs (smoke test)")
143
+ ap.add_argument("--batch_size", type=int, default=8)
144
+ ap.add_argument("--grad_accum", type=int, default=4)
145
+ ap.add_argument("--warmup_steps", type=int, default=10)
146
+ ap.add_argument("--eval_limit", type=int, default=0, help="evaluate on the first N val rows only (0 = all)")
147
+ ap.add_argument("--eval_batch", type=int, default=16, help="candidate sequences scored per forward pass")
148
+ ap.add_argument("--eval_steps", type=int, default=0,
149
+ help="also evaluate every N optimizer steps (0 = only at epoch ends). "
150
+ "Use it when the model converges early: the best checkpoint may be mid-epoch.")
151
+ ap.add_argument("--skip_baseline", action="store_true")
152
+ ap.add_argument("--eval_only", action="store_true", help="just score the model on --val (zero-shot baseline)")
153
+ ap.add_argument("--seed", type=int, default=3407)
154
+ args = ap.parse_args()
155
+ if not args.eval_only and not args.train:
156
+ sys.exit("--train is required unless --eval_only")
157
+
158
+ from unsloth import FastModel # must come before transformers / trl / peft
159
+ import torch
160
+ from datasets import Dataset
161
+ from transformers import DataCollatorForSeq2Seq, TrainerCallback
162
+ from trl import SFTConfig, SFTTrainer
163
+
164
+ random.seed(args.seed)
165
+ os.makedirs(args.out, exist_ok=True)
166
+ json.dump(vars(args), open(os.path.join(args.out, "run_config.json"), "w"), indent=1)
167
+ print(f"[model] loading {args.model}")
168
+ model, processor = FastModel.from_pretrained(model_name=args.model, dtype=None,
169
+ max_seq_length=args.max_seq_length,
170
+ load_in_4bit=False, full_finetuning=False)
171
+ tok = getattr(processor, "tokenizer", processor)
172
+ if tok.pad_token is None:
173
+ tok.pad_token = tok.eos_token
174
+
175
+ val_rows = load_rows(args.val)
176
+ print(f"[data] val rows: {len(val_rows)}")
177
+
178
+ if args.eval_only:
179
+ res = evaluate(model, tok, val_rows, args.max_seq_length, args.eval_limit, args.eval_batch)
180
+ json.dump(res, open(os.path.join(args.out, "eval_only.json"), "w"), ensure_ascii=False, indent=1)
181
+ return
182
+
183
+ model = FastModel.get_peft_model(
184
+ model, finetune_vision_layers=False, finetune_language_layers=True,
185
+ finetune_attention_modules=True, finetune_mlp_modules=True,
186
+ r=args.lora_r, lora_alpha=args.lora_alpha, lora_dropout=0, bias="none", random_state=args.seed,
187
+ use_gradient_checkpointing="unsloth")
188
+ try:
189
+ model.print_trainable_parameters()
190
+ except Exception:
191
+ pass
192
+
193
+ train_rows = load_rows(args.train)
194
+ enc, dropped = [], 0
195
+ for r in train_rows:
196
+ ids, labels = encode_example(tok, r["prompt"], r["completion"])
197
+ if len(ids) > args.max_seq_length:
198
+ dropped += 1
199
+ continue
200
+ enc.append({"input_ids": ids, "attention_mask": [1] * len(ids), "labels": labels})
201
+ print(f"[data] train examples: {len(enc)} (dropped {dropped} longer than {args.max_seq_length} tokens)")
202
+ train_ds = Dataset.from_list(enc)
203
+
204
+ coll_kw = dict(padding=True, label_pad_token_id=-100, pad_to_multiple_of=8)
205
+ coll_params = inspect.signature(DataCollatorForSeq2Seq.__init__).parameters
206
+ if "tokenizer" in coll_params:
207
+ collator = DataCollatorForSeq2Seq(tokenizer=tok, **coll_kw)
208
+ else: # transformers >=5 renamed it
209
+ collator = DataCollatorForSeq2Seq(processing_class=tok, **coll_kw)
210
+ # --- masking sanity check: only the answer tokens may carry a label
211
+ b = collator([enc[0], enc[min(1, len(enc) - 1)]])
212
+ frac = float((b["labels"] != -100).float().mean())
213
+ shown = tok.decode([t for t in b["labels"][0].tolist() if t != -100])
214
+ print(f"[mask] labelled-token fraction {frac:.3f}; example 0 trains on: {shown!r}")
215
+ if frac > 0.3:
216
+ sys.exit("Masking looks wrong (too many labelled tokens). Stop and check the data format.")
217
+
218
+ history = []
219
+ best = {"acc": -1.0}
220
+
221
+ def record(tag, res, step):
222
+ history.append({"tag": tag, "step": step, **res})
223
+ json.dump(history, open(os.path.join(args.out, "history.json"), "w"), ensure_ascii=False, indent=1)
224
+
225
+ if not args.skip_baseline:
226
+ print("[baseline] zero-shot accuracy before training")
227
+ record("baseline", evaluate(model, tok, val_rows, args.max_seq_length, args.eval_limit, args.eval_batch), 0)
228
+
229
+ class EpochEval(TrainerCallback):
230
+ last = -1
231
+
232
+ def _run(self, tag, state, model):
233
+ if state.global_step == self.last:
234
+ return
235
+ self.last = state.global_step
236
+ res = evaluate(model, tok, val_rows, args.max_seq_length, args.eval_limit, args.eval_batch)
237
+ record(tag, res, state.global_step)
238
+ if res["acc"] > best["acc"]:
239
+ best.update(res, step=state.global_step)
240
+ d = os.path.join(args.out, "best_adapter")
241
+ model.save_pretrained(d)
242
+ processor.save_pretrained(d)
243
+ print(f"[save] new best accuracy {res['acc']:.4f} -> {d}")
244
+
245
+ def on_step_end(self, a, state, control, model=None, **kw):
246
+ if args.eval_steps > 0 and state.global_step > 0 and state.global_step % args.eval_steps == 0:
247
+ self._run(f"step {state.global_step}", state, model)
248
+
249
+ def on_epoch_end(self, a, state, control, model=None, **kw):
250
+ self._run(f"epoch {state.epoch:.2f}", state, model)
251
+
252
+ def on_train_end(self, a, state, control, model=None, **kw):
253
+ self._run("end", state, model)
254
+
255
+ cfg_kw = dict(
256
+ output_dir=os.path.join(args.out, "trainer"), per_device_train_batch_size=args.batch_size,
257
+ gradient_accumulation_steps=args.grad_accum, learning_rate=args.lr, lr_scheduler_type="cosine",
258
+ warmup_steps=args.warmup_steps, weight_decay=0.01, max_grad_norm=1.0,
259
+ num_train_epochs=args.epochs, max_steps=args.max_steps, bf16=True, optim="adamw_8bit",
260
+ logging_steps=10, save_strategy="no", seed=args.seed, report_to="none",
261
+ remove_unused_columns=False, dataset_text_field="", max_length=args.max_seq_length)
262
+ if "dataset_kwargs" in inspect.signature(SFTConfig.__init__).parameters:
263
+ cfg_kw["dataset_kwargs"] = {"skip_prepare_dataset": True}
264
+ trainer = SFTTrainer(model=model, processing_class=tok, train_dataset=train_ds,
265
+ data_collator=collator, args=SFTConfig(**cfg_kw), callbacks=[EpochEval()])
266
+ stats = trainer.train()
267
+ print(f"[done] {stats.metrics.get('train_runtime', 0) / 60:.1f} min; "
268
+ f"peak VRAM {torch.cuda.max_memory_reserved() / 1024 ** 3:.1f} GB")
269
+
270
+ final = os.path.join(args.out, "final_adapter")
271
+ model.save_pretrained(final)
272
+ processor.save_pretrained(final)
273
+ print(f"[save] final adapter -> {final}")
274
+ print("\nValidation accuracy history:")
275
+ for h in history:
276
+ print(f" {h['tag']:>12s} step {h['step']:5d} acc {h['acc']:.4f} (n={h['n']}, "
277
+ f"majority-position baseline {h['majority_baseline']:.3f})")
278
+ print(f"best: {best['acc']:.4f} at step {best.get('step')} -> {os.path.join(args.out, 'best_adapter')}")
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()