Qwen3-1.7B Frontend SFT
A QLoRA supervised fine-tune of Qwen/Qwen3-1.7B for front-end code generation: complete HTML documents, CSS, JavaScript, and whole-file edits.
The repository root holds the merged 16-bit model (loads directly, no PEFT
needed). The LoRA adapter alone is under adapter/.
Usage
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "panzarasa/qwen3-1.7b-frontend-sft"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype="bfloat16", device_map="auto")
messages = [
{"role": "system", "content": "You are an expert front-end engineer. Given a website description, reply with a single complete HTML document with its CSS in a <style> block."},
{"role": "user", "content": "A landing page for a coffee roastery: hero, three product cards, newsletter form, footer."},
]
ids = tok.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to(model.device)
out = model.generate(ids, max_new_tokens=8192, temperature=0.7, top_p=0.9, repetition_penalty=1.05)
print(tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True))
Sampling settings
temperature=0.7, top_p=0.9, repetition_penalty=1.05, max_new_tokens=8192
These are a reasonable default, not a fix for the failure mode below. Both 0.3 and 0.7 were measured on the same six held-out pages and scored the same; the rows that fail differ, the count does not.
Known failure mode: long pages may not terminate
On long full-page generation (the webcode2m style: a real-world page with a
trailing <style> block), the model can fall into a repetition loop and burn
the whole token budget without emitting </html>. Two shapes were observed:
- textual -- a sentence or a counter repeats (
vitamin B580,B581, ...); - structural -- markup cycles (one generated page held 518
<li>elements drawn from 8 distinct variants).
Measured on 6 random held-out webcode2m rows, at both temperatures: 4 of 6
reached </html>; 2 of 6 hit the 8192-token cap. Raising the temperature does
not reliably help. Detect it rather than prevent it: check that the output ends
in </html> and retry if it does not.
Short outputs -- single components, JavaScript functions, whole-file edits, the
websight style of compact Tailwind page -- did not show this in testing.
Prompt format
The model was trained with the
Qwen/Qwen3-4B-Instruct-2507
chat template, not the hybrid-thinking template that ships with Qwen3-1.7B.
The tokenizer in this repo already carries the right one. There are no
<think> blocks in the output.
System prompts seen during training:
You are an expert front-end engineer. Given a website description, reply with a single complete HTML document ...You are an expert front-end engineer. Given a page brief, reply with a single complete HTML document with its CSS in a <style> block.You are a senior front-end engineer. Answer with correct, production-ready HTML, CSS and JavaScript. Be concise.You are an expert front-end engineer. Apply the requested change to the file and output only the complete updated file. No explanation, no markdown fences.
Training
| Base | Qwen/Qwen3-1.7B (loaded 4-bit nf4 via unsloth/Qwen3-1.7B-unsloth-bnb-4bit) |
| Method | QLoRA, r=32, alpha=32, dropout 0 |
| Target modules | q, k, v, o, gate, up, down |
| Trainable params | 34,865,152 (2.03% of 1.72B) |
| Sequence length | 8,192 (prompt + answer) |
| Epochs | 1 (16,701 steps) |
| Effective batch | 16 (2 x grad-accum 2 x 4 GPUs) |
| LR | 2e-4, cosine decay |
| Loss on | assistant tokens only |
| Hardware | 4x RTX 4090 24GB, 4h 33m |
| Final eval loss | 0.3713 |
Rows longer than 8,192 tokens were dropped, not truncated, so the model never saw an unterminated document.
Data
267,202 training rows / 1,981 validation rows, mixed from:
| Source | Rows | Mean tokens | What it teaches |
|---|---|---|---|
bigcode/commitpackft | 61,312 | 475 | whole-file edits from a change request |
HuggingFaceM4/WebSight | 60,000 | 557 | description to Tailwind page |
xcodemind/webcode2m | 59,998 | 4,544 | long real-world pages with a <style> block |
Tesslate/Next.js-Dataset | 49,954 | 1,601 | Next.js / React answers with explanation |
saurabh5/rlvr-code-data-JavaScript-sft | 40,000 | 363 | JavaScript exercises |
glyphsoftware/opus-4.6-frontend-development | 715 | 1,297 | front-end conversations |
runanlab/gpt-5.4-frontend-development-27052026 | 665 | 6,551 | long front-end builds |
Each source keeps its own licence; check them before redistributing derived data.
Evaluation
Eval loss decreased monotonically over the single epoch and flattened at the end:
| epoch | eval loss |
|---|---|
| 12% | 0.4757 |
| 24% | 0.4384 |
| 36% | 0.4168 |
| 48% | 0.3983 |
| 60% | 0.3886 |
| 72% | 0.3769 |
| 84% | 0.3711 |
| 100% | 0.3713 |
Held-out generation
One held-out row per source was generated and read by hand:
| Source | Result |
|---|---|
websight | valid Tailwind page, closed, semantic header/main/footer |
saurabh_js | correct lucas(n) with base cases and input validation, code only |
Tesslate | valid Next.js App Router POST handler, 400/500 paths, matches the source's prose register |
commitpackft | whole updated file returned, 11/11 lines, no diff, no fences, requested attributes added |
webcode2m | see the failure mode above |
Six further random webcode2m rows were generated at two temperatures and
scored on closure, <style> presence, tag balance and repeated blocks. Model:
2/6 at either temperature. The reference pages score 3/6 on the same
metric -- real scraped pages are themselves repetitive, so the gap is narrower
than the raw number suggests.
Training data duplication
xcodemind/webcode2m is a web scrape, and the same site template recurs many
times. Hashing each page's tag sequence (structure only, ignoring text and
attributes) across the 59,566 training rows:
- 40,373 distinct structures; 32.2% of the rows are structural duplicates
- 419 structures appear 10+ times; the largest cluster holds 171 copies
There is no exact train/val leakage (0 identical documents), but 52 of the 390 distinct validation page titles also occur in training. The effect is visible in generation: on two of the six sampled rows the model reproduces the held-out reference at 99.6% and 98.1% similarity, including a 2,091-char verbatim span. Temperature does not suppress it.
Practical consequence: eval loss understates the true generalization gap on long pages, and output for this kind of page may closely resemble a real website. Deduplicating by structural hash before the split would remove ~19,000 rows (~20% of all training tokens) and is the first change worth making.
Limitations
- Long full-page generation fails to terminate roughly 1 time in 3; check for
</html>and retry. - Can reproduce training pages near-verbatim on the
webcode2mstyle -- see Training data duplication. Do not assume a generated page is original. - Can skip elements a brief explicitly asks for (a
<form>was missed in one held-out page that requested one). - Trained for one epoch; eval loss had flattened under a cosine schedule decayed to 1e-6. More epochs on this data would deepen the duplication problem, not fix it -- deduplicate first.
- English only.
- 1.7B parameters: expect scaffolding and structure, not production copy.