diff --git a/docs/docs/models/granite.md b/docs/docs/models/granite.md
new file mode 100644
index 00000000..024c380c
--- /dev/null
+++ b/docs/docs/models/granite.md
@@ -0,0 +1,29 @@
+---
+layout: docs
+title: Granite
+nav_order: 14
+parent: Models
+---
+
+## 🧩 Model Card: [ibm-granite/granite-4.2-3b](https://huggingface.co/ibm-granite/granite-4.2-3b)
+
+- **Type:** Text-to-Text
+- **Think:** Yes
+- **Tool Calling Support:** Yes
+- **Base Model:** [ibm-granite/granite-4.2-3b](https://huggingface.co/ibm-granite/granite-4.2-3b)
+- **Quantization:** Q4_1
+- **Max Context Length:** 128k tokens
+- **Default Context Length:** 8k tokens ([change default](https://fastflowlm.com/docs/instructions/cli/#-change-default-context-length-max))
+- **[Set Context Length at Launch](https://fastflowlm.com/docs/instructions/cli/#-set-context-length-at-launch)**
+
+▶️ Run with FastFlowLM in PowerShell:
+
+```shell
+flm run granite:3b
+```
+
+Granite 4.2 is a reasoning model: the chat template opens a `` block in
+the generation prompt, so the model emits a reasoning trace, closes it with
+``, and then answers.
+
+---
diff --git a/docs/docs/models/index.md b/docs/docs/models/index.md
index ea9613c7..fedb3b5e 100644
--- a/docs/docs/models/index.md
+++ b/docs/docs/models/index.md
@@ -27,4 +27,5 @@ has_children: true
- [Nanbeige](nanbeige/)
- [Whisper](whisper/)
- [EmbeddingGemma](embeddinggemma/)
-- [SmolVLA](smolvla/)
\ No newline at end of file
+- [SmolVLA](smolvla/)
+- [Granite](granite/)
\ No newline at end of file
diff --git a/src/common/AutoModel/modeling_granite.cpp b/src/common/AutoModel/modeling_granite.cpp
new file mode 100644
index 00000000..d1188676
--- /dev/null
+++ b/src/common/AutoModel/modeling_granite.cpp
@@ -0,0 +1,86 @@
+/// \file modeling_granite.cpp
+/// \brief IBM Granite (dense) family. See modeling_granite.hpp.
+
+#include "AutoModel/modeling_granite.hpp"
+
+/************ Granite family **************/
+Granite::Granite(flm_rt::device* npu_device_inst) : AutoModel(npu_device_inst, "Granite") {}
+
+void Granite::load_model(std::string model_path, json model_info, int default_context_length,
+ bool enable_preemption) {
+ this->_shared_load_model(model_path, model_info, default_context_length, enable_preemption);
+
+ this->q4nx = std::make_unique(this->model_path);
+ this->lm_engine = std::make_unique(*this->lm_config, this->npu.get(), this->MAX_L);
+
+ this->lm_engine->load_weights(*this->q4nx);
+ this->q4nx.reset();
+
+ this->lm_engine->clear_context();
+ this->setup_tokenizer(model_path);
+ this->sampler.reset();
+
+ // granite-4.2 is a reasoning model; its own generation_config ships
+ // temperature 1.0 / top_p 0.95, and a repetition guard keeps long chains of
+ // thought from looping.
+ sampler_config config;
+ config.top_k = 40;
+ config.top_p = 0.95;
+ config.min_p = 0.0;
+ config.temperature = 1.0;
+ config.rep_penalty = 1.05;
+
+ this->set_sampler(config);
+ for (size_t i = 0; i < PROFILER_TYPE_NUM; i++) {
+ this->profiler_list[i].reset();
+ }
+}
+
+void Granite::setup_tokenizer(std::string model_path) {
+ auto tokenizer_config = this->_shared_setup_tokenizer(model_path);
+}
+
+std::string Granite::apply_chat_template(nlohmann::ordered_json& messages,
+ nlohmann::ordered_json tools) {
+ minja::chat_template_inputs inputs;
+ inputs.add_generation_prompt = true;
+ inputs.messages = messages;
+ inputs.extra_context = this->extra_context;
+ return this->chat_tmpl->apply(inputs);
+}
+
+bool Granite::insert(chat_meta_info_t& meta_info, lm_uniform_input_t& input,
+ std::function is_cancelled) {
+ this->profiler_list[TKOEN_ENCODE_TIME].start();
+ std::string templated_text;
+ if (input.messages.empty() && input.prompt.empty()) {
+ header_print("WARNING", "No messages or prompt provided");
+ return false;
+ }
+ if (!input.messages.empty()) {
+ templated_text = this->apply_chat_template(input.messages);
+ }
+ else if (!input.prompt.empty()) {
+ nlohmann::ordered_json messages;
+ messages.push_back({ {"role", "user"}, {"content", input.prompt} });
+ templated_text = this->apply_chat_template(messages);
+ }
+
+ std::vector tokens = this->tokenizer->encode(templated_text);
+ this->profiler_list[TKOEN_ENCODE_TIME].stop(tokens.size());
+
+ return this->_shared_insert(meta_info, tokens, is_cancelled);
+}
+
+std::string Granite::generate(chat_meta_info_t& meta_info, int length_limit, std::ostream& os,
+ std::function is_cancelled) {
+ return this->_shared_generate(meta_info, length_limit, os, is_cancelled);
+}
+
+std::string Granite::generate_with_prompt(chat_meta_info_t& meta_info, lm_uniform_input_t& input,
+ int length_limit, std::ostream& os) {
+ if (!this->insert(meta_info, input)) {
+ return "";
+ }
+ return this->_shared_generate(meta_info, length_limit, os);
+}
diff --git a/src/common/models/granite_npu.cpp b/src/common/models/granite_npu.cpp
new file mode 100644
index 00000000..90048dfc
--- /dev/null
+++ b/src/common/models/granite_npu.cpp
@@ -0,0 +1,559 @@
+/// \file granite_npu.cpp
+/// \brief Host implementation of the Granite dense engine. See granite_npu.hpp.
+///
+/// Numerics follow the model the converter produced, not Granite's paper form.
+/// q4nx-build folds Granite's four scalar multipliers into the weights, so what
+/// the file holds is a Llama-scaled model and the emitted `config.json` says so
+/// (`attention_multiplier` becomes the post-fold `head_dim ** -0.5`). Reading
+/// the scale from the config therefore works for both a folded and an unfolded
+/// build, which is why it is read rather than assumed.
+///
+/// q_proj / k_proj are stored in the PLAIN half-split arrangement, so nothing
+/// has to be undone at load time. An earlier version of this file un-permuted
+/// them on the belief that the converter had interleaved them, and that was the
+/// bug that made the model ramble: the un-permutation introduced exactly the
+/// scrambling it thought it was removing.
+///
+/// It was measured, not argued. Against a numpy forward pass that produces
+/// correct text from these same bytes, layer 0 reads:
+///
+/// norm_in cosine 1.00000000 (same input)
+/// q cosine -0.01162270 |q| 54.092 vs 54.091
+/// k cosine -0.08337054 |k| 302.411 vs 302.420
+/// v cosine 0.99999977
+///
+/// Identical norms with cosine ~0 is a permutation, not an arithmetic error,
+/// and it hit exactly the two tensors that were being permuted while v, which
+/// was not, matched to 1e-7.
+///
+/// The claim that the permutation had been "verified against
+/// q4nx-build/tools/oracle_granite.py" was true and worthless: that oracle
+/// reproduces the same broken output, because it makes the same assumption.
+
+#include "models/granite/granite_npu.hpp"
+#include "models/granite/q4nx_host.hpp"
+#include "modules/gemm.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include